Dynamic Sticky Sorting in Mongo for a simple value or list

匿名 (未验证) 提交于 2019-12-03 09:52:54

问题:

I'm trying to dynamically sticky sort a collection of records with the value that is sticky being different with each query. Let me give an example. Here are some example docs:

{first_name: 'Joe', last_name: 'Blow', offices: ['GA', 'FL']} {first_name: 'Joe', last_name: 'Johnson', offices: ['FL']} {first_name: 'Daniel', last_name: 'Aiken', offices: ['TN', 'SC']} {first_name: 'Daniel', last_name: 'Madison', offices: ['SC', 'GA']} ... a bunch more names ... 

Now suppose I want to display the names in alphabetical order by last name but I want to peg all the records with the first name "Joe" at the top.

In SQL this is fairly straight forward:

SELECT * FROM people ORDER first_name == 'Joe' DESC, last_name 

The ability to put expressions in the sort criteria makes this trivial. Using the aggregation framework I can do this:

[   {$project: {     first_name: 1,     last_name: 1     offices: 1,     sticky: {$cond: [{$eq: ['$first_name', 'Joe']}, 1, 0]}   }},   {$sort: [     'sticky': -1,     'last_name': 1   ]} ] 

Basically I create a dynamic field with the aggregation framework that is 1 if the name if Joe and 0 if the name is not Joe then sort in reverse order. Of course when building my aggregation pipeline I can easily change 'Joe' to be 'Daniel' and now 'Daniel' will be pegged to the top. This is partially what I mean by dynamic sticky sorting. The value I am sticky sorting by will change query-by-query

Now this works great for a basic value like a string. The problem comes when I try to the same thing for a value that hold an array. Say I want to peg all users in 'FL' offices. With Mongo's native understanding of arrays I would think I can do the same thing. So:

[   {$project: {     first_name: 1,     last_name: 1     offices: 1,     sticky: {$cond: [{$eq: ['$offices', 'FL']}, 1, 0]}   }},   {$sort: [     'sticky': -1,     'last_name': 1   ]} ] 

But this doesn't work at all. I did figure out that if I changed it to the following it would put Joe Johnson (who is only in the FL office) at the top:

[   {$project: {     first_name: 1,     last_name: 1     offices: 1,     sticky: {$cond: [{$eq: ['$offices', ['FL']]}, 1, 0]}   }},   {$sort: [     'sticky': -1,     'last_name': 1   ]} ] 

But it didn't put Joe Blow at the top (who is in FL and GA). I believe it is doing simple match. So my first attempt doesn't work at all since $eq returns false since we are comparing an array to a string. The second attempt works for Joe Johnson because we are comparing the exact same arrays. But Joe Blow doesn't work since ['GA', 'FL'] != ['FL']. Also if I want to peg both FL and SC at the top I can't give it the value ['FL', 'SC'] to compare against.

Next I try using a combination of $setUnion and $size.

[   {$project: {     first_name: 1,     last_name: 1     offices: 1,     sticky: {$size: {$setUnion: ['$offices', ['FL', 'SC']]}}   }},   {$sort: [     'sticky': -1,     'last_name': 1   ]} ] 

I've tried using various combinations of $let and $literal but it always complains about me trying to pass a literal array into $setUnion's arguments. Specifically it says:

disallowed field type Array in object expression 

Is there any way to do this?

回答1:

Cannot reproduce your error but you have a few "typos" in your question so I cannot be sure what you actually have.

But presuming you actually are working with MongoDB 2.6 or above then you probably want the $setIntersection or $setIsSubset operators rather than $setUnion. Those operators imply "matching" contents of the array they are compared to, where $setUnion just combines the supplied array with the existing one:

db.people.aggregate([     { "$project": {         "first_name": 1,         "last_name": 1,         "sticky": {              "$size": {                  "$setIntersection": [ "$offices", [ "FL", "SC" ]]              }         },         "offices": 1     }},     { "$sort": {         "sticky": -1,         "last_name": 1     }} ]) 

In prior versions where you do not have those set operators you are just using $unwind to work with the array, and the same sort of $cond operation as before within a $group to bring it all back together:

db.people.aggregate([     { "$unwind": "$offices" },     { "$group": {         "_id": "$_id",         "first_name": { "$first": "$first_name" },         "last_name": { "$first": "$last_name",         "sticky": { "$sum": { "$cond": [             { "$or": [                 { "$eq": [ "$offices": "FL" ] },                 { "$eq": [ "$offices": "SC" ] },             ]},             1,             0         ]}},         "offices": { "$push": "$offices" }     }},     { "$sort": {         "sticky": -1,         "last_name": 1     }} ]) 

But you were certainly on the right track. Just choose the right set operation or other method in order to get your precise need.


Or since you have posted your way of getting what you want, a better way to write that kind of "ordered matching" is this:

db.people.aggregate([     { "$project": {         "first_name": 1,         "last_name": 1,         "sticky": { "$cond": [             { "$anyElementTrue": {                 "$map": {                     "input": "$offices",                     "as": "o",                     "in": { "$eq": [ "$$o", "FL" ] }                 }             }},             2,             { "$cond": [                 { "$anyElementTrue": {                     "$map": {                         "input": "$offices",                         "as": "o",                         "in": { "$eq": [ "$$o", "SC" ] }                     }                 }},                 1,                 0             ]}         ]},         "offices": 1     }},     { "$sort": {         "sticky": -1,         "last_name": 1     }} ]) 

And that would give priority it documents with "offices" containing "FL" over "SC" and hence then over all others, and doing the operation within a single field. That should also be very easy for people to see how to abstract that into the form using $unwind in earlier versions without the set operators. Where you simply provide the higher "weight" value to the items you want at the top by nesting the $cond statements.



回答2:

I think I figured out the best way to do it.

[   {$project: {     first_name: 1,     last_name: 1     offices: 1,     sticky_0: {       $cond: [{         $anyElementTrue: {           $map: {             input: "$offices",             as:    "j",             in:    {$eq: ['$$j', 'FL']}           }         }       }, 0, 1]     },     sticky_1": {       $cond: [{         $anyElementTrue: {           $map: {             input: '$offices',             as:    'j',             in:    {$eq: ['$$j', 'SC']}           }         }       }, 0, 1]     }   }},   {$sort: [     'sticky_0': 1,     'sticky_1': 1,     'last_name': 1   ]} ] 

Basically when building my pipeline I iterate through each item I want to make sticky and from that item create it's own virtual field that checks just the one value. To check just the one value I use a combination of $cond, $anyElementTrue and $map. It's a little convoluted but it works. Would love to hear if there is something simpler.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!