Let’s say I have a collection with a Boolean field isFoo
.
I want to return ALL Docs - but, I want all isFoo
s sorted first. Then - all non-isFoos
would be after.
That simple, merely sort:{ isFoo:1}
would suffice.
However, things get tricky, because I want all isFoos
(already at the top), to then be sorted in an order unique to just isFoos. Then - my second set of non-isFoos will also have their own unique sort.
I’ve solved this is JS by just getting all Docs, splitting the full collection by isFoo, sorting each array, and then merging into one final one.
let docs = Docs.find({});
const foos = [], nonFoos = [];
docs.forEach(doc => (doc.isfoo ? foos : nonFoos).push(doc));
docs = [..._.sortBy(foos, ['fooSortField1', 'fooSortField2']), ..._.sortBy(nonFoos, ['nonFooSortField1', 'nonFooSortField2'])];
How can I do this in a query? Thanks!
You can pass in multiple sorts:
sort: {
isFoo: 1,
secondSortField: 1
}
If you have a field (e.g., fooOnlyField
) that is unique to isFoo == true
and another (e.g., notFooOnlyField
) field that is unique to isFoo == false
- you could pass in all three fields to sort, then isFoo
documents would be sorted by fooOnlyField
and !isFoo
documents would be sorted by notFooNlyField
This was the first thing I tried, but then realized it affects the sort of the nonFoos
.
i.e.
sort: {
isFoo: -1, //bc I want all Foos at top
fooSortField1: 1,
fooSortField2: 1,
nonFooSortField1: 1,
nonFooSortField2: 1
}
This doesn’t work bc then my nonFoos are sorted by fooSortField1&2. I just want the second set (nonFoos) to be sorted by nonFooSortField1&2. So it’s conditional unfortunately.
Fortunately, all fields exist on every document. I just want “All foos at top. And…within all foos…sort by fooSortField1 asc …then fooSortField2 asc. Then - all nonFoos. Sorted by nonFooSortField1 asc and then nonFooSortField2…”
I hope I’m making sense
In this case I don’t believe you can do it. Technically you could probably do it with an aggregation, but there isn’t a lot of point, and it wouldn’t be reactive. I’d probably issue two queries, one to get the sorted foos, one to get the sorted non-foos, then concat them. That way no _.sortBy is required.
docs = [
...Docs.find({ isFoo: true }, { sort: { fooField1: 1, fooField2: 1 } }),
...Docs.find({ isFoo: false }, { sort: { notFoo1: 1, notFoo2: 1 } })
]
1 Like
Figured I’d need an aggregate. Using rawCollection
's aggregate isn’t reactive?
Since your solution still uses two cursors, I’d still get reactivity, right?
(Also to @znewsham)
You can use tunguska:reactive-aggregate. However, make sure you install version 1.3.3 rather than it’s current 1.3.5 due to a bug just recently discovered in that version.
v1.3.6 was released yesterday which reverts the change which introduced the bug. Normal service is resumed ![:slightly_smiling_face: :slightly_smiling_face:](https://emoji.discourse-cdn.com/twitter/slightly_smiling_face.png?v=9)
5 Likes
I think I may use this package. Any ideas on how to achieve this with an aggregate though? I spent some time fumbling around with MongoDB Compass GUI but can’t get what I need. I looked into $merge
and $out
but I think that’s overkill. I’m not trying to output to another collection
. I basically just need something like SQL union
.
Looks like $union
is made for two separate collections though. I’m querying the same one.
What you are looking for is the $unionWith
stage. It would look something like that:
const results = Docs.rawCollection().aggregate([
{ $match: { isFoo: true } },
{ $sort: { fooA: 1, fooB: 1 } },
{
$unionWith: {
coll: Docs._name, // Collection name in the DB.
pipeline: [
{ $match: { isFoo: false } },
{ $sort: { notFooA: 1, notFooB: 1 } },
],
},
},
]);
EDIT: It can be simplified (without performance hit):
const results = Docs.rawCollection().aggregate([
// Make sure there will be no results.
{ $limit: 1 },
{ $skip: 1 },
// $unionWith all queries.
{
$unionWith: {
coll: Docs._name, // Collection name in the DB.
pipeline: [
{ $match: { isFoo: true } },
{ $sort: { fooA: 1, fooB: 1 } },
],
},
},
{
$unionWith: {
coll: Docs._name, // Collection name in the DB.
pipeline: [
{ $match: { isFoo: false } },
{ $sort: { notFooA: 1, notFooB: 1 } },
],
},
},
]);
1 Like
$unionWith
isn’t even an operator in MongoDB Compass lol. I’ll have to try this in the CLI (tends to be a pain bc it’s hard to type out )
Update: Meteor Mongo CLI gives me "errmsg" : "Unrecognized pipeline stage name: '$unionWith'",
Meteor instance is running on Mongo v4.0.2 btw
The $unionWith
pipeline stage is fairly new - it was added in Mongo 4.4.
1 Like
Ah, that explains it. Any workarounds for Mongo 4.0.2?
You could do a $facet
with $match
+$sort
in separate branches, but I’m pretty sure it’s going to be slower - do test it though, it may work for your case.
1 Like
I’ll look into. Any reason I shouldn’t just update Meteor Mongo to the latest? Any risk of regression ? Thanks !
You cannot upgrade the MongoDB used in Meteor development mode. At least as far as I know.
But if you’d use a dockerized one or the one in your staging/production environment - no, not at all. Always check the change log, but in general MongoDB updates are forward-compatible.
1 Like