7

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?

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
bymannan
  • 1,353
  • 2
  • 13
  • 23
  • You seem to be trying to de-normalize, and you most probably do not want to do this. Especially for the results you are trying to achieve. Also like your last question to seem to be generalizing your concepts. Try asking a question that has your **actual use case**. Then you might get might get some different thinking on your problem. – Neil Lunn Mar 05 '14 at 08:32
  • @NeilLunn Thanks. I've refactored the question completely in hope it would be clearer. – bymannan Mar 05 '14 at 08:59
  • That's better. So you have an API call that returns a list. And it's going to be small right? I API call is just populating an array. What you don't understand is how to use this in the query. Right? – Neil Lunn Mar 05 '14 at 09:28

2 Answers2

11

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

Community
  • 1
  • 1
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • Thanks! I am not clear on how this would make all documents from user_id 4 to bubble above all the documents from user_id 1? If I understand correctly, weight will always be 1 for users in the list. No? – bymannan Mar 05 '14 at 09:57
  • @bymannan Yes. Unless you need to distinguish a higher user than another then the matches just get 1 and everything else gets 0. If you need a value, then use $cond **around** the $eq for each item in the $or Also stuck in some ruby code for reference. – Neil Lunn Mar 05 '14 at 10:13
  • Thank you very much. So the $or can return any value? According the documentation it's only true false:"Takes an array of one or more values and returns true if any of the values in the array are true. Otherwise $or returns false." – bymannan Mar 05 '14 at 10:18
  • @bymannan Ah. Sorry Do you need assigned weights for each user_id ? – Neil Lunn Mar 05 '14 at 10:20
  • Yes, yes... The weight can be the index in the users array so that user_id 4 is the most important and user_id 1 is the least. Or can be a wright from outside. Don't mind. – bymannan Mar 05 '14 at 10:22
  • @bymannan Okay. Then it probably has to be nested $cond rather than $or. You can think about that while I do as well. – Neil Lunn Mar 05 '14 at 10:29
  • Thank you for the answer! I was thinking about another way. First project weight0 for all the users in the list, and then project weight1 for user_id:1, weight2 for user_id:9 and so on and on. Then to sort all weights in order. Will that work? Does sort takes into account the order of the arguments? Is there a performance issue? – bymannan Mar 05 '14 at 10:40
  • @bymannan The hard thing there is by the nature of aggregation you can't per-say check if the **current** field value already has a value.So you'd just be interchanging only 1 user_id with a weight. There is probably a way, but it would be just as obtuse. If not more. – Neil Lunn Mar 05 '14 at 10:57
  • @bymannan If you were still struggling with the code to generate, I posted this [here](http://stackoverflow.com/a/22213246/2313887) – Neil Lunn Mar 06 '14 at 04:09
0

Since mongoDB version 3.2 we can use a $filter which make this much easier to maintain in case there are more than 4 scores:

db.collection.aggregate([
  {
    $addFields: {
      weight: [
        {key: 4, score: 10}, {key: 8, score: 9}, {key: 90, score: 8}, {key: 1, score: 7}       
      ]
    }
  },
  {
    $addFields: {
      weight: {
        $filter: {
          input: "$weight",
          as: "item",
          cond: {$eq: ["$$item.key", "$user_id"]}
        }
      }
    }
  },
  {
    $set: {
      weight: {
        $cond: [{$eq: [{$size: "$weight"}, 1]}, {$arrayElemAt: ["$weight", 0]}, {score: 1}]
      }
    }
  },
  {$set: {weight: "$weight.score"}},
  {$sort: {weight: -1, date: -1}}
])

See how it works on the playground example

nimrod serok
  • 14,151
  • 2
  • 11
  • 33