Mongo DB: Sorting by the number of matches - arrays

I have an array of objects, and I want to query in a MongoDB collection for documents that have elements that match any objects in my array of objects.
For example:
var objects = ["52d58496e0dca1c710d9bfdd", "52d58da5e0dca1c710d9bfde", "52d91cd69188818e3964917b"];
db.scook.recipes.find({products: { $in: objects }}
However, I want to know if I can sort the results by the number of matches in MongoDB.
For example, at the top will be the "recipe" that has exactly three elements matches: ["52d58496e0dca1c710d9bfdd", "52d58da5e0dca1c710d9bfde", "52d91cd69188818e3964917b"].
The second selected has two recipes: i.e. ["52d58496e0dca1c710d9bfdd", "52d58da5e0dca1c710d9bfde"], and the third one only one: i.e. ["52d58496e0dca1c710d9bfdd"]
It would be great if you could get the number of items it had.

By using the aggregation framework, I think that you should be able to get what you need by the following MongoDB query. However, if you're using Mongoose, you'll have to convert this to a Mongoose query. I'm not certain this will work exactly as is, so you may need to play with it a little to make it right. Also, this answer hinges on whether or not you can use the $or operator inside of the $project operator and that it will return true. If that doesn't work, I think you'll need to use map-reduce to get what you need or do it server side.
db.recipes.aggregate(
// look for matches
{ $match : { products : { $or : objects }}},
// break apart documents to by the products subdocuments
{ $unwind : "$products" },
// search for matches in the sub documents and add productMatch if a match is found
{ $project : {
desiredField1 : 1,
desiredField2 : 1,
products : 1,
// this may not be a valid comparison, but should hopefully
// be true or 1 if there is a match
productMatch : { "$products" : { $or : objects }}
}},
// group the unwound documents back together by _id
{ $group : {
_id : "$_id",
products : { $push : "$products" },
// count the matched objects
numMatches : { $sum : "$productMatch" },
// increment by 1 for each product
numProducts : { $sum : 1 }
}},
// sort by descending order by numMatches
{ $sort : { numMatches : -1 }}
)

Related

How to set existing field and their value to array's objects

I'm new to MongoDB. I've an object below
{
"_id" : "ABCDEFGH1234",
"level" : 0.6,
"pumps" : [
{
"pumpNo" : 1
},
{
"pumpNo" : 2
}
]
}
And I just want to move level field to pumps array's objects like this
{
"_id" : "ABCDEFGH1234",
"pumps" : [
{
"pumpNo" : 1,
"level" : 0.6
},
{
"pumpNo" : 2,
"level" : 0.6
}
]
}
I've check on MongoDB doc in Aggregation section but didn't found anything. In SQL by JOIN or SUB Query I'm able to do but here it's No-SQL
Could you please help me with this? Thankyou
Try this on for size:
db.foo.aggregate([
// Run the existing pumps array through $map and for each
// item (the "in" clause), create a doc with the existing
// pumpNo and bring in the level field from doc. All "peer"
// fields to 'pumps' are addressable as $field.
// By $projecting to a same-named field (pumps), we effectively
// overwrite the old pumps array with the new.
{$project: {pumps: {$map: {
input: "$pumps",
as: "z",
in: {pumpNo:"$$z.pumpNo", level:"$level"}
}}
}}
]);
Strongly recommend you explore the power of $map, $reduce, $concatArrays, $slice, and other array functions that make MongoDB query language different from the more scalar-based approach in SQL.

Query where all array items are greater than a specified condition

I have a model which contains an array with dates. I'm using a $gte operator as a condition to query the collection where all the elements in the array of dates are $gte a given date.
For example I have this document:
{ dates: [
ISODate("2016-10-24T22:00:00.000+0000"),
ISODate("2017-01-16T23:00:00.000+0000")]
}
When I run this query {dates: {$gte: new Date()}}, it gives me the whole document as a result. But I want a result where every single array item matches my query, not just one.
You can do this by using $not and the inversion of your comparison condition:
db.test.find({dates: {$not: {$lt: new Date()}}})
So this matches docs where it's not the case that there's a dates element with a value less than the current time; in other words, all dates values are >= the current time.
You can also use the aggregation framework with the $redact pipeline operator that allows you to proccess the logical condition with the $cond operator and uses the special operations $$KEEP to "keep" the document where the logical condition is true or $$PRUNE to "remove" the document where the condition was false.
This operation is similar to having a $project pipeline that selects the fields in the collection and creates a new field that holds the result from the logical condition query and then a subsequent $match, except that $redact uses a single pipeline stage which is more efficient.
As for the logical condition, there are Set Operators that you can use since they allow expression that perform set operations on arrays, treating arrays as sets. These couple of these operators namely the $allElementTrue and $map operators can be used as the logical condition expression as they work in such a way that if all of the elements in the array actually are $gte a specified date, then this is a true match and the document is "kept". Otherwise it is "pruned" and discarded.
Consider the following examples which demonstrate the above concept:
Populate Test Collection
db.test.insert([
{ dates: [
ISODate("2016-10-24T22:00:00.000+0000"),
ISODate("2017-01-16T23:00:00.000+0000")]
} ,
{ dates: [
ISODate("2017-01-03T22:00:00.000+0000"),
ISODate("2017-01-16T23:00:00.000+0000")]
}
])
$redact with $setEquals
db.test.aggregate([
{ "$match": { "dates": { "$gte": new Date() } } },
{
"$redact": {
"$cond": [
{
"$allElementsTrue": {
"$map": {
"input": "$dates",
"as": "date",
"in": { "$gte": [ "$$date", new Date() ] }
}
}
},
"$$KEEP",
"$$PRUNE"
]
}
}
])
Sample Output
{
"_id" : ObjectId("581899dda450d81cb7d87d3a"),
"dates" : [
ISODate("2017-01-03T22:00:00.000Z"),
ISODate("2017-01-16T23:00:00.000Z")
]
}
Another not-so elegant approach would be to use $where (as a last resort) with the Array.prototype.every() method:
db.test.find({ "$where": function(){
return this.dates.every(function(date) {
return date >= new Date();
})}
})

Fetch specific array elements from a array element within another array field in mongodb

My document structure is as below.
{
"_id" : {
"timestamp" : ISODate("2016-08-27T06:00:00.000+05:30"),
"category" : "marketing"
},
"leveldata" : [
{
"level" : "manager",
"volume" : [
"45",
"145",
"2145"
]
},
{
"level" : "engineer",
"volume" : [
"2145"
]
}
]
}
"leveldata.volume" embedded array document field can have around 60 elements in it.
In this case, "leveldata" is an array document.
And "volume" is another array field inside "leveldata".
We have a requirement to fetch specific elements from the "volume" array field.
For example, elements from specific positions, For Example, position 1-5 within the array element "volume".
Also, we have used positional operator to fetch the specific array element in this case based on "leveldata.level" field.
I tried using the $slice operator. But, it seems to work only with arrays not with array inside array fields, as that
is the case in my scenario.
We can do it from the application layer, but that would mean loading the entire the array element from mongo db to memory and
then fetching the desired elements. We want to avoid doing that and fetch it directly from mongodb.
The below query is what I had used to fetch the elements as required.
db.getCollection('mycollection').find(
{
"_id" : {
"timestamp" : ISODate("2016-08-26T18:00:00.000-06:30"),
"category" : "sales"
}
,
"leveldata.level":"manager"
},
{
"leveldata.$.volume": { $slice: [ 1, 5 ] }
}
)
Can you please let us know your suggestions on how to address this issue.
Thanks,
mongouser
Well yes you can use $slice to get that data like
db.getCollection('mycollection').find({"leveldata.level":"manager"} , { "leveldata.volume" : { $slice : [3 , 1] } } )

How to find a document with array size within a range in MongoDB?

Given the following document structure:
{
_id : Mongo ID,
data : [someData1, someData2, ..., someDataN]
}
I want to get all documents which data size is between a and b (strict positive integers, consider a < b is always true).
I first thought about using $size, but the Mongo doc states:
$size does not accept ranges of values. To select documents based on fields with different numbers of elements, create a counter field that you increment when you add elements to a field.
(Emphasis mine)
How to retrieve all the documents with a data array of length between a and b without using a counter?
Related but did not solve:
This answer which suggests using a counter
This question which asks how to query an internal length
and got many answers
This answer which suggests to use: { 'data.' + b : { $exists : true } } but I don't know how reliable it is and how it could apply to a range
This question about how to find documents with a minimal array length
You can use the $exists operator approach by including multiple terms in your query and building it up programmatically:
var startKey = 'data.' + (a-1);
var endKey = 'data.' + b;
var query = {};
query[startKey] = {$exists: true};
query[endKey] = {$exists: false};
db.test.find(query);
So for a=1 and b=3, for example, you end up with a query object that looks like:
{
"data.0" : {
"$exists" : true
},
"data.3" : {
"$exists" : false
}
}
The first part ensures there are at least a elements, and the second part ensures there are no more than b elements.

Return documents of which field of type array contains array in exact order

I'm having documents in MongoDB structured like this:
{ _id : 1, tokens : [ "one","two","three","four","five","six","seven" ] }
{ _id : 2, tokens : [ "two","three","four","one","one","five","eight" ] }
{ _id : 3, tokens : [ "six","three","four","five","one","five","nine" ] }
On average the documents contain token arrays with a length of 4500 items.
I need to do some sort of pattern matching, where I have arrays of tokens in exact order to match, i.e. let's say I have to find the following in exactly matching order...
["three","four","five"]
...I want my query to provide me the following documents...
{ _id : 1, tokens : [ "one","two","three","four","five","six","seven" ] }
{ _id : 3, tokens : [ "six","three","four","five","one","five","nine" ] }
I.e. both documents contain the exact order of the items I had in my array to search with.
Arrays I search with may have different lengths, ranging from 1 to 15 tokens.
I'm looking for the following:
Is this doable with MongoDB queries? I've read, and re-read and re-re-read the pretty good docs, but couldn't find a solution e.g. using $all.
Is there perhaps a better way to store tokens like this to get done what I need?
Thanks for any help.
It's going to be slow, but you can do this with a $where operator; pairing it with an $all operator to help with performance.
db.test.find({
tokens: {$all: ["three","four","five"]},
$where: function() {
var ix = -1;
// Find each occurrence of 'three' in this doc's tokens array and return
// true if it's followed by 'four' and 'five'.
do {
ix = this.tokens.indexOf('three', ix + 1);
if (ix !== -1 && ix+2 < this.tokens.length &&
this.tokens[ix+1] === 'four' && this.tokens[ix+2] === 'five') {
return true;
}
} while (ix !== -1);
return false;
}
})

Resources