0

I have a schema like this:

{
   tags:[
      {
         id:"t1",
         score:70
      },
      {
         id:"t1",
         score:60
      }
   ]
 }

I want to sort the query on tag.id search to get sorted by the corresponding score. So if I search db.collection.find({tags.id:"t1"}).sort({tags.score:-1}) , it sorted by score of the "t1" object not other tags.

Any suggestion?

1
  • Might have made a clearer question if your array sample contained other values than just "t1", and possibly more than one document and an expected result. But I think I see what you are getting at. Commented Nov 8, 2015 at 9:38

2 Answers 2

1

If you need to calculate out something like this at runtime, with "filtered" content from the array determining the sort order, then you best do something with .aggregate() to reshape and determine a sort value like this:

db.collection.aggregate([
    // Pre-filter the array elements
    { "$project": {
        "tags": 1,
        "score": {
            "$setDifference": [
                { "$map": {
                    "input": "$tags",
                    "as": "tag",
                    "in": {
                        "$cond": [
                            { "$eq": [ "$$el.id", "t1" ] },
                            "$$el.score",
                            false
                        ]
                    }
                }},
                [false]
            ]
        }
    }},
    // Unwind to denormalize
    { "$unwind": "$score" },
    // Group back the "max" score
    { "$group": {
        "_id": "$_id",
        "tags": { "$first": "$tags" },
        "score": { "$max": "$score" }
    }},
    // Sort descending by score
    { "$sort": { "score": -1 } }
])

Where the first part of the pipeline is used to "pre-filter" the array content ( as well as keeping the original field ) to just those values of "score" where the id is equal to "t1". This is done by processing $map which applies a condition to each element via $cond to determine whether to return the "score" for that element or false.

The $setDifference operation does a comparison to a single element array [false] which effectively removes any false values returned from the $map. As a "set", this also removes duplicate entries, but for the sort purpose here this is a good thing.

With the array reduced and reshaped to values you process $unwind ready for the next stage to deal with the values as individual elements. The $group stage essentially applies $max on the "score" to return the highest value contained in the filtered results.

Then it is just a matter of applying the $sort on the determined value to order the documents. Naturaly if you wanted this the other way around then use $min and sort in ascending order instead.

Of course add a $match stage to the beginning if all you really want is documents that actually contain "t1" values for id within the tags. But that part is of least relevance to the sorting on filtered results you want to achieve.

The alternate to calculating is to do it all as you write entries to the array in the documents. Kind of messy, but it goes something like this:

db.collection.update(
    { "_id": docId },
    {
        "$push": { "tags": { "id": "t1", "score": 60 } },
        "$max": { "maxt1score": 60 },
        "$min": { "mint1score": 60 }
    }
)

Here the $max update operator only sets the value for the specified field if the new value is greater than the existing value or otherwise no property yet exists. The reverse case is true of $min, where only if less than it will be replaced with the new value.

This would of course have the effect of adding various additional properties to the documents, but the end result is sorting is greatly simplified:

db.collection.find().sort({ "maxt1score": -1 })

And it's going to run a lot faster than calculating with an aggregation pipeline.

So consider the design principles. Structured data in arrays where you want filtered and paired results for sorting means calculating at run-time to determine which value to sort on. Adding additional properties to the document on .update() means you can simply reference those properties in order to directly sort results.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the explanation. The second approach is not good for my case cause I can have 50-100 variations of tags. Although it's really efficient. The aggregate function gave me an error that el is not defined and when turn it to $el it shows no result, am I missing something here?
Worked as expected with a small edit in your answer.
How can I get the whole document back in the group? should I add them one by one in projection?
That is generally a requirement of $project, that you specify each field. The unaltered array was included in the answer as an example of this. You can always use $$ROOT as a shortcut, but this would essentially change the structure.
0

According to above mentioned description the query

db.collection.find({tags.id:"t1"}).sort({tags.score:-1}) 

will filter the documents consisting of tag id 1 and will return the results sorted by score in descending order.

Also please provide detailed description along with example document and expected output results

1 Comment

Fairly sure that the OP's question clearly states that the "sort" is meant to be applied to values in the array matching "t1" only. This will not do that and only matches documents containing "t1" and does not restrict the sort as such. At least it would if the "dot notation" syntax were correct here and not illegal. But it's still not an answer to the question that was asked.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.