mongodb query and sort by list item - mongodb

How to sort documents by funnelSteps[0].count, that is how to sort by the count number of the first funnelSteps?
Thank you.
{
"funnelSteps" : [{
"title" : "step1",
"criteria" : ["1","2"],
"count" : 305
}, {
"title" : "step2",
"criteria" : ["1","2","3"],
"count" : 153
}]
}

MongoDB uses "dot notation" to refer to nested elements in a structure, so you can indeed specify an element by index:
db.collection.find().sort({ "funnelSteps.0.count": 1 })
Where the sort order of 1 is ascending or -1 for descending. See .sort() for more detail.
That is fine for a "known" position of an array element, but if you wanted to sort by something such as the "least" value within "funnelSteps" then you would do something like this using .aggregate():
db.collection.aggregate([
{ "$unwind": "$funnelSteps" },
{ "$group": {
"_id": "$_id",
"funnelSteps": { "$push": "$funnelSteps" },
"lowestCount": { "$min": "$funnelSteps.count" }
}},
{ "$sort": { "lowestCount": 1 } }
])
So in that case you would need to "pull apart" the array in order to get the value you wanted before sorting. But for a known position you can just use the basic arguments to sort as shown.

Related

MongoDB sort by value in embedded document array

I have a MongoDB collection of documents formatted as shown below:
{
"_id" : ...,
"username" : "foo",
"challengeDetails" : [
{
"ID" : ...,
"pb" : 30081,
},
{
"ID" : ...,
"pb" : 23995,
},
...
]
}
How can I write a find query for records that have a challengeDetails documents with a matching ID and sort them by the corresponding PB?
I have tried (this is using the NodeJS driver, which is why the projection syntax is weird)
const result = await collection
.find(
{ "challengeDetails.ID": challengeObjectID},
{
projection: {"challengeDetails.$": 1},
sort: {"challengeDetails.0.pb": 1}
}
)
This returns the correct records (documents with challengeDetails for only the matching ID) but they're not sorted.
I think this doesn't work because as the docs say:
When the find() method includes a sort(), the find() method applies the sort() to order the matching documents before it applies the positional $ projection operator.
But they don't explain how to sort after projecting. How would I write a query to do this? (I have a feeling aggregation may be required but am not familiar enough with MongoDB to write that myself)
You need to use aggregation to sort n array
$unwind to deconstruct the array
$match to match the value
$sort for sorting
$group to reconstruct the array
Here is the code
db.collection.aggregate([
{ "$unwind": "$challengeDetails" },
{ "$match": { "challengeDetails.ID": 2 } },
{ "$sort": { "challengeDetails.pb": 1 } },
{
"$group": {
"_id": "$_id",
"username": { "$first": "$username" },
"challengeDetails": { $push: "$challengeDetails" }
}
}
])
Working Mongo playground

Mongodb aggregate distinct with sort and limit

I have a collection objects.
{
"_id" : ObjectId("55fa65046db58e7d0c8b456a"),
"object_id" : "1651419",
"user" : {
"id" : "65593",
"cookie" : "9jgkm7ME1HDFD4K6j8WWvg",
},
"createddate" : ISODate("2015-09-17T10:00:20.945+03:00")
}
Every time user visits object's page it stores as separate record in collection. Now i need to get array of last N visited objects. It should be distinct, so array should have N unique records. Also, it should be sorted by createddate.
So if the user visited object_id = 1, then object_id = 2 two times, after that visited object_id = 3 and again object_id = 1 the array should contain:
{
visits : [1, 3, 2]
}
(distinct and sorted by time of last visit).
I tried to use construction like
db.objects.aggregate([
{$match: {'user.id' : '65593'}},
{$sort: { 'createddate':-1 }},
{$project: {'id': '$user.id', 'obj' : '$object_id'}},
{$group: {_id:'$id', 'obj': {$addToSet: '$obj'}}},
{$project:{_id:0, 'obj':'$obj'}}
])
but it returns array that not sorted and also i can't limit array size.
The $addToSet operator and "sets" in general for MongoDB are not ordered in any way. Insead, get the "distinct" values by grouping on them first, then apply to the array after sorting them:
db.objects.aggregate([
{ "$match": { "user.id": "65593" } },
{ "$sort": { "user.id": 1, "createddate": -1 } },
{ "$group": {
"_id": {
"_id": "$user.id",
"object_id": "$object_id"
},
"createddate": { "$first": "$createddate" }
}},
{ "$sort": { "_id._id": 1, "createddate": -1 } },
{ "$group": {
"_id": "$_id._id",
"obj": { "$push": "$_id.object_id" }
}}
])
So if you want the discovery oder by date you $sort first, but since $group does not guarantee any order of results you need to $sort again before you group with the $push operation to build the array.
Note that you are likely reducing down the "createddate" somehow as then general "distinct" items would appear to be the "user.id" and the "object_id" fields, so this does need some sort of accumulator and needs to be included for your ordering.
Then the array items will be in the order you expect.
If you need to $limit then you must process $unwind and split the limit the results. Alternately process a "limit" after the first group and following sort here.
But of course this is only practical to do for a single main grouping _id, being "user.id". Future mongodb releases will support $slice, which will make this practical for multiple grouping id's and a bit more simple in general. But it still won't be possible to "limit" the array items before that initial group over multiple primary groupind id's.
I found the solution i expected.
db.objects.aggregate([
{$match: {'user.id' : '65593'}},
{$group : {
_id : '$object_id',
dt : {$max: '$createddate'}
}
},
{$sort: {'dt':-1}},
{$limit:5},
{$group : {
_id :null,
'objects' : {$push:'$_id'}
}
},
{$project: {_id:0, 'objects':'$objects'}}
])
It returns limited to N distinct array sorted backwards by createddate.
Thank everyone for help!

MongoDB : limit query to a field and array projection

I have a collection that contains following information
{
"_id" : 1,
"info" : { "createdby" : "xyz" },
"states" : [ 11, 10, 9, 3, 2, 1 ]}
}
I project only states by using query
db.jobs.find({},{states:1})
Then I get only states (and whole array of state values) ! or I can select only one state in that array by
db.jobs.find({},{states : {$slice : 1} })
And then I get only one state value, but along with all other fields in the document as well.
Is there a way to select only "states" field, and at the same time slice only one element of the array. Of course, I can exclude fields but I would like to have a solution in which I can specify both conditions.
You can do this in two ways:
1> Using mongo projection like
<field>: <1 or true> Specify the inclusion of a field
and
<field>: <0 or false> Specify the suppression of the field
so your query as
db.jobs.find({},{states : {$slice : 1} ,"info":0,"_id":0})
2> Other way using mongo aggregation as
db.jobs.aggregate({
"$unwind": "$states"
}, {
"$match": {
"states": 11
}
}, // match states (optional)
{
"$group": {
"_id": "$_id",
"states": {
"$first": "$states"
}
}
}, {
"$project": {
"_id": 0,
"states": 1
}
})

Query number of sub collections Mongodb

I am new to mongodb and I am trying to figure out how to count all the returned query inside an array of documents like below:
"impression_details" : [
{
"date" : ISODate("2014-04-24T16:35:46.051Z"),
"ip" : "::1"
},
{
"date" : ISODate("2014-04-24T16:35:53.396Z"),
"ip" : "::1"
},
{
"date" : ISODate("2014-04-25T16:22:20.314Z"),
"ip" : "::1"
}
]
What I would like to do is count how many 2014-04-24 there are (which is 2). At the moment my query is like this and it is not working:
db.banners.find({
"impression_details.date":{
"$gte": ISODate("2014-04-24T00:00:00.000Z"),
"$lte": ISODate("2014-04-24T23:59:59.000Z")
}
}).count()
Not sure what is going on please help!
Thank you.
The concept here is that there is a distinct difference between selecting documents and selecting elements of a sub-document array. So what is happening currently in your query is exactly what should be happening. As the document contains at least one sub-document entry that matches your condition, then that document is found.
In order to "filter" the content of the sub-documents itself for more than one match, then you need to apply the .aggregate() method. And since you are expecting a count then this is what you want:
db.banners.aggregate([
// Matching documents still makes sense
{ "$match": {
"impression_details.date":{
"$gte": ISODate("2014-04-24T00:00:00.000Z"),
"$lte": ISODate("2014-04-24T23:59:59.000Z")
}
}},
// Unwind the array
{ "$unwind": "$impression_details" },
// Actuall filter the array contents
{ "$match": {
"impression_details.date":{
"$gte": ISODate("2014-04-24T00:00:00.000Z"),
"$lte": ISODate("2014-04-24T23:59:59.000Z")
}
}},
// Group back to the normal document form and get a count
{ "$group": {
"_id": "$_id",
"impression_details": { "$push": "$impression_details" },
"count": { "$sum": 1 }
}}
])
And that will give you a form that only has the elements that match your query in the array, as well as providing the count of those entries that were matched.
Use the $elemMatch operator would do what you want.
In your query it meas to find all the documents whose impression_details field contains a data between ISODate("2014-04-24T00:00:00.000Z") and ISODate("2014-04-24T23:59:59.000Z"). The point is, it will return the whole document which is not what you want. So if you want only the subdocuments that satisfies your condition:
var docs = db.banners.find({
"impression_details": {
$elemMatch: {
data: {
$gte: ISODate("2014-04-24T00:00:00.000Z"),
$lte: ISODate("2014-04-24T23:59:59.000Z")
}
}
}
});
var count = 0;
docs.forEach(function(doc) {
count += doc.impression_details.length;
});
print(count);

How to sort by 'value' of a specific key within a property stored as an array with k-v pairs in mongodb

I have a mongodb collection, let's call it rows containing documents with the following general structure:
{
"setid" : 154421,
"date" : ISODate("2014-02-22T14:06:48.229Z"),
"version" : 2,
"data" : [
{
"k" : "name",
"v" : "ryan"
},
{
"k" : "points",
"v" : "375"
},
{
"k" : "email",
"v" : "ryan#123.com"
}
],
}
There is no guarantee what values of k and v might populate the "data" property for any particular document (eg. other documents might have 5 k-v pairs with different key names in it). The only rule is that documents with the same setid have the same k-v pairs. (i.e. the rows collection might hold 100 other documents with setid = 154421, that have the same set of 3 keys in the data property: "name", "points", "email", with their own respective values.
How would one, with this setup, construct a query to retrieve all rows with a particular setid sorted by points? I need, in effect, some way of saying 'sort by the the field data.v where the value of k==points or something like that...?
Something like this:
db.rows.find({setid:154421},{$sort:{'data.v',-1}, {$where: k:'points'}}})
I know this is the incorrect syntax, but I'm just taking a stab at it to illustrate my point.
Is it possible?
Assuming that what you want would be all the documents that have the "points" value as a "key" in the array, and then sort on the "value" for that "key", then this is a little out of scope for the .find() method.
Reason being if you did something like this
db.collection.find({
"setid": 154421, "data.k": "point" }
).sort({ "data.v" : -1 })
The problem is that even though the matched elements do have the matching key of "point", there is no way of telling which data.v you are referring to for the sort. Also, a sort within .find() results will not do something like this:
db.collection.find({
"setid": 154421, "data.k": "point" }
).sort({ "data.$.v" : -1 })
Which would be trying to use a positional operator within a sort, essentially telling which element to use the value of v on. But this is not supported and not likely to be, and for the most likely explaination, that "index" value would be likely different in every document.
But this kind of selective sorting can be done with the use of .aggregate().
db.collection.aggregate([
// Actually shouldn't need the setid
{ "$match": { "data": {"$elemMatch": { "k": "points" } } } },
// Saving the original document before you filter
{ "$project": {
"doc": {
"_id": "$_id",
"setid": "$setid",
"date": "$date",
"version": "$version",
"data": "$data"
},
"data": "$data"
}}
// Unwind the array
{ "$unwind": "$data" },
// Match the "points" entries, so filtering to only these
{ "$match": { "data.k": "points" } },
// Sort on the value, presuming you want the highest
{ "$sort": { "data.v": -1 } },
// Restore the document
{ "$project": {
"setid": "$doc.setid",
"date": "$doc.date",
"version": "$doc.version",
"data": "$doc.data"
}}
])
Of course that presumes the data array only has the one element that has the key points. If there were more than one, you would need to $group before the sort like this:
// Group to remove the duplicates and get highest
{ "$group": {
"_id": "$doc",
"value": { "$max": "$data.v" }
}},
// Sort on the value
{ "$sort": { "value": -1 } },
// Restore the document
{ "$project": {
"_id": "$_id._id",
"setid": "$_id.setid",
"date": "$_id.date",
"version": "$_id.version",
"data": "$_id.data"
}}
So there is one usage of .aggregate() in order to do some complex sorting on documents and still return the original document result in full.
Do some more reading on aggregation operators and the general framework. It's a useful tool to learn that takes you beyond .find().