Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mongo: how to sort by external weight

Following this question which @NeilLunn has gracefully answered, here is my problem in more detail.

This is the set of documents, some have user_id some don't. The user_id represent the user who created the document:

{ "user_id" : 11, "content" : "black", "date": somedate }
{ "user_id" : 6, "content" : "blue", "date": somedate }
{ "user_id" : 3, "content" : "red", "date": somedate }
{ "user_id" : 4, "content" : "black", "date": somedate }
{ "user_id" : 4, "content" : "blue", "date": somedate }
{ "user_id" : 90, "content" : "red", "date": somedate }
{ "user_id" : 7, "content" : "orange", "date": somedate }
{ "content" : "orange", "date": somedate }
{ "content" : "red", "date": somedate }
...
{ "user_id" : 4, "content" : "orange", "date": somedate }
{ "user_id" : 1, "content" : "orange", "date": somedate }
{ "content" : "red", "date": somedate }
{ "user_id" : 90, "content" : "purple", "date": somedate }

The front end is pulling pages, so each page will have 10 items and I do that with limit and skip and it is working very well.

In case we have a logged in user, I would like to display to that current logged in user documents which he may find more interesting first, based on the users he interacted with.

The list of users which the current user may find interesting is sorted by score and is located outside of mongo. So the first element is the most important user which I would like to show his documents first, and the last user on the list is the least important.

The list is a simple array which looks like this: [4,7,90,1].

The system which created this user score is not located within mongo, but I can copy the data if that will help. I can also change the array to include a score number.

What I would like accomplish is the following:

Get the documents sorted by importance of the user_id from the list, so that documents from user_id 4 will be the first to show up, documents from user_id 7 second and so on. When where are no users left on the list I would like to show the rest of the documents. Like this:

  1. all documents with user_d:4
  2. all documents with user_d:7
  3. all documents with user_d:90
  4. all documents with user_d:1
  5. all the rest of the documents

How should I accomplish this? Am I asking too much from mongo?

like image 385
bymannan Avatar asked Mar 05 '14 08:03

bymannan


People also ask

How do I sort a limit in MongoDB?

Sorting with the limit() methodThe sort() method can be used along with the limit() method that limits the number of results in the search query. You should pass an integer to the limit() method, which then specifies the number of documents to which the result set should be limited.

Does MongoDB support sorting?

MongoDB may use multiple indexes to support a sort operation if the sort uses the same indexes as the query predicate. If MongoDB cannot use an index or indexes to obtain the sort order, MongoDB must perform a blocking sort operation on the data.

How do I sort a case insensitive in MongoDB?

This means the only way to sort case insensitive currently is to actually create a specific "lower cased" field, copying the value (lower cased of course) of the sort field in question and sorting on that instead.


1 Answers

Given the array [4,7,90,1] what you want in your query is this:

db.collection.aggregate([
   { "$project": {
       "user_id": 1,
       "content": 1,
       "date": 1,
       "weight": { "$or": [
           { "$eq": ["$user_id": 4] }, 
           { "$eq": ["$user_id": 7] }, 
           { "$eq": ["$user_id": 90] }, 
           { "$eq": ["$user_id": 1] }, 
       ]}
   }},
   { "$sort": { "weight": -1, "date": -1 } }
])

So what that does is, for every item contained in that $or condition, the user_id field is tested against the supplied value, and $eq returns 1 or 0 for true or false.

What you do in your code is for each item you have in the array you build the array condition of $or. So it's just creating a hash structure for each equals condition, passing it to an array and plugging that in as the array value for the $or condition.

I probably should have left the $cond operator out of the previous code so this part would have been clearer.

Here's some code for the Ruby Brain:

userList = [4, 7, 90, 1];

orCond = [];

userList.each do |userId|
  orCond.push({ '$eq' => [ 'user_id', userId ] })
end

pipeline = [
    { '$project' => {
        'user_id' => 1,
        'content' => 1,
        'date' => 1,
        'weight' => { '$or' => orCond }
    }},
    { '$sort' => { 'weight' => -1, 'date' => -1 } }
]

If you want to have individual weights and we'll assume key value pairs, then you need to nest with $cond :

db.collection.aggregate([
   { "$project": {
       "user_id": 1,
       "content": 1,
       "date": 1,
       "weight": { "$cond": [
           { "$eq": ["$user_id": 4] },
           10,
           { "$cond": [ 
               { "$eq": ["$user_id": 7] },
               9,
               { "$cond": [
                   { "$eq": ["$user_id": 90] },
                   7,
                   { "$cond": [
                       { "$eq": ["$user_id": 1] },
                       8, 
                       0
                   ]}
               ]}
           ]}
       ]}
   }},
   { "$sort": { "weight": -1, "date": -1 } }
])

Note that it's just a return value, these do not need to be in order. And you can think about the generation of that.

For generating this structure see here:

https://stackoverflow.com/a/22213246/2313887

like image 84
Neil Lunn Avatar answered Nov 10 '22 05:11

Neil Lunn