Is there something like a not-any operator for mongodb-queries?
I have the following setup:
const ConstructQuestion = new Schema({
answerType: String,
text: String,
});
const Construct = new Schema({
name: String,
description: String,
questions: [ConstructQuestion],
});
I also have an array of ConstructQuestion-IDs. I now try to query all Constructs, that contain any questions whose _id
is not in my ConstructQuestionID-Array.
In other words: I want all Constructs that have questions whose _id
is not any of the IDs in my array
I can achieve the opposite of what I want using $in
, which queries all Constructs, that have any questions whose _id
is any of the IDs in my ID-array. sadly $nin
wasn't the answer, as it doesn't match a Construct that contains all the questions in my ID-Array and more
edit:
To be very precise: I'm looking for a query that will give me all Constructs that contain at least one question whose id is not in my ID-Array
result:
With the help of Bertrands answer my mongoose-solution now looks like this:
return this.construct.aggregate()
.group({
_id: '$_id',
questions_id: {$push: '$questions._id'},
construct: {$first: '$$ROOT'},
})
.unwind('$questions_id')
.project({
construct: true,
questions_id: true,
check: {
$cmp: [{
$size: {$setIntersection: ['$questions_id', usedQuestionIds]},
}, {
$size: '$questions_id',
}],
},
})
.match({check: {$lt: 0}})
.exec()
.then((results) => {
const constructs = results.map((result) => {
return result.construct;
});
})
improvement:
After adding the questionIds as array directly to the Construct on every create/update, I could remove the group
and unwind
stages, so my code now looks like this:
return this.construct.aggregate({
$addFields: {
check: {
$cmp: [{
$size: {$setIntersection: ['$questionIds', usedQuestionIds]},
}, {
$size: '$questionIds',
}],
},
}})
.match({check: {$lt: 0}})
.exec()
.then((constructs) => {
});
Thanks again to Bertrand for this hint
improvement 2
Again, as a hint from Bertrand, $project
and $match
can (in this case) be combined to $redact
, with which the code now looks like this:
return this.construct.aggregate({
$redact: {
$cond: {
if: {
$lt: [
{$size: {$setIntersection: ['$questionIds', usedQuestionIds]}},
{$size: '$questionIds'}
]
},
then: '$$KEEP',
else: '$$PRUNE'
}
}})
.exec()
.then((constructs) => {
});
improvement 3
I found out, that I could use $setDifference and a single $size
instead of $setInsersection
and two $size
. The code now looks like this:
return this.construct.aggregate({
$redact: {
$cond: {
if: {
$gt: [{$size: {$setDifference: ['$questionIds', usedQuestionIds]}}, 0]
},
then: "$$KEEP",
else: "$$PRUNE"
}
}})
.exec()
.then((constructs) => {
});
I think what you are looking for is $not
+ $all
:
var input = ["58c20f6d95b16e3046ddd7e9", "58c20f6d95b16e3046ddd7eb", "58c20f6d95b16e3046ddd7ec"];
Data.find({
"questions._id": {
"$not": {
"$all": input
}
}
}, function(err, res) {
console.log(res);
});
It will return all document that haven't got all questions specified in input input
array. If the document is selected, it means at least one question in the input
array is not in the document.
As you want the intersection to be exact match for the questions
document, you can use setIntersection
available in aggregation to determine elements which are common to both array and take decision according to that :
var array = [
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7e9"),
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7ea"),
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7eb"),
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7ec")
];
Const.aggregate([{
$group: {
"_id": "$_id",
"questions_id": {
$push: "$questions._id"
},
"document": { $first: "$$ROOT" }
}
}, {
$unwind: "$questions_id"
}, {
$project: {
document: 1,
questions_id: 1,
check: {
$cmp: [{
$size: {
$setIntersection: ["$questions_id", array]
}
}, {
$size: "$questions_id"
}]
}
}
}, {
$match: {
check: {
"$lt": 0
}
}
}], function(err, res) {
console.log(res);
});
The first $group
& $unwind
take care of building an array of questions._id
. The comparison is performed in the next $project
with the intersection size and the size of questions._id
.
You can simplify $project
+ $match
with a $redact
that will keep dcouments matching the condition. With your newly added questionIds
field, it's much simpler :
var array = [
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7e9"),
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7ea"),
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7eb"),
new mongoose.mongo.ObjectId("58c20f6d95b16e3046ddd7ec")
];
Const.aggregate([{
$redact: {
$cond: {
if: {
$lt: [{
$size: {
$setIntersection: ["$questionIds", array]
}
}, {
$size: "$questionIds"
}]
},
then: "$$KEEP",
else: "$$PRUNE"
}
}
}], function(err, res) {
console.log(res);
});