Czy w przypadku korzystania z $inklauzuli MongoDB kolejność zwracanych dokumentów zawsze odpowiada kolejności argumentów tablicy?
Czy w przypadku korzystania z $inklauzuli MongoDB kolejność zwracanych dokumentów zawsze odpowiada kolejności argumentów tablicy?
Odpowiedzi:
Jak zauważono, kolejność argumentów w tablicy klauzuli $ in nie odzwierciedla kolejności, w jakiej dokumenty są pobierane. Będzie to oczywiście naturalna kolejność lub wybrana kolejność indeksu, jak pokazano.
Jeśli chcesz zachować tę kolejność, zasadniczo masz dwie opcje.
Powiedzmy więc, że dopasowujesz wartości _idw swoich dokumentach z tablicą, która zostanie przekazana do $inas [ 4, 2, 8 ].
var list = [ 4, 2, 8 ];
db.collection.aggregate([
// Match the selected documents by "_id"
{ "$match": {
"_id": { "$in": [ 4, 2, 8 ] },
},
// Project a "weight" to each document
{ "$project": {
"weight": { "$cond": [
{ "$eq": [ "$_id", 4 ] },
1,
{ "$cond": [
{ "$eq": [ "$_id", 2 ] },
2,
3
]}
]}
}},
// Sort the results
{ "$sort": { "weight": 1 } }
])
Więc to byłaby rozszerzona forma. Zasadniczo dzieje się tutaj to, że tak jak tablica wartości jest przekazywana do $inCiebie, również konstruujesz „zagnieżdżony”$cond instrukcję, aby przetestować wartości i przypisać odpowiednią wagę. Ponieważ ta wartość „wagi” odzwierciedla kolejność elementów w tablicy, możesz następnie przekazać tę wartość do etapu sortowania, aby uzyskać wyniki w wymaganej kolejności.
Oczywiście faktycznie „budujesz” instrukcję potoku w kodzie, podobnie jak to:
var list = [ 4, 2, 8 ];
var stack = [];
for (var i = list.length - 1; i > 0; i--) {
var rec = {
"$cond": [
{ "$eq": [ "$_id", list[i-1] ] },
i
]
};
if ( stack.length == 0 ) {
rec["$cond"].push( i+1 );
} else {
var lval = stack.pop();
rec["$cond"].push( lval );
}
stack.push( rec );
}
var pipeline = [
{ "$match": { "_id": { "$in": list } }},
{ "$project": { "weight": stack[0] }},
{ "$sort": { "weight": 1 } }
];
db.collection.aggregate( pipeline );
Oczywiście, jeśli to wszystko wydaje się mocne dla twojej wrażliwości, możesz zrobić to samo za pomocą mapReduce, które wygląda na prostsze, ale prawdopodobnie będzie działać nieco wolniej.
var list = [ 4, 2, 8 ];
db.collection.mapReduce(
function () {
var order = inputs.indexOf(this._id);
emit( order, { doc: this } );
},
function() {},
{
"out": { "inline": 1 },
"query": { "_id": { "$in": list } },
"scope": { "inputs": list } ,
"finalize": function (key, value) {
return value.doc;
}
}
)
Zasadniczo polega to na tym, że emitowane wartości „klucza” są w „kolejności indeksów”, w jakiej występują w tablicy wejściowej.
Więc to są w istocie sposoby na utrzymanie kolejności listy wejściowej do $instanu, w którym masz już tę listę w określonej kolejności.
Inny sposób użycia kwerendy agregacji ma zastosowanie tylko do wersji MongoDB> = 3.4 -
To zasługa tego miłego wpisu na blogu .
Przykładowe dokumenty do pobrania w tej kolejności -
var order = [ "David", "Charlie", "Tess" ];
Zapytanie -
var query = [
{$match: {name: {$in: order}}},
{$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
{$sort: {"__order": 1}}
];
var result = db.users.aggregate(query);
Kolejny cytat z postu wyjaśniający zastosowane operatory agregacji -
Etap „$ addFields” jest nowością w wersji 3.4 i umożliwia „projektowanie” nowych pól w istniejących dokumentach bez znajomości wszystkich innych istniejących pól. Nowe wyrażenie "$ indexOfArray" zwraca pozycję konkretnego elementu w danej tablicy.
Zasadniczo addFieldsoperator dołącza nowe orderpole do każdego dokumentu, gdy je znajdzie, a to orderpole reprezentuje oryginalną kolejność podanej przez nas tablicy. Następnie po prostu sortujemy dokumenty na podstawie tego pola.
Jeśli nie chcesz używać aggregate, innym rozwiązaniem jest użycie, finda następnie posortowanie wyników dokumentu po stronie klienta za pomocą array#sort:
Jeśli $inwartości są typami pierwotnymi, takimi jak liczby, możesz użyć podejścia takiego:
var ids = [4, 2, 8, 1, 9, 3, 5, 6];
MyModel.find({ _id: { $in: ids } }).exec(function(err, docs) {
docs.sort(function(a, b) {
// Sort docs by the order of their _id values in ids.
return ids.indexOf(a._id) - ids.indexOf(b._id);
});
});
Jeśli $inwartości nie są typami pierwotnymi, takimi jak ObjectIds, wymagane jest inne podejście, ponieważ indexOfw tym przypadku porównuje się przez odniesienie.
Jeśli używasz Node.js 4.x +, możesz użyć Array#findIndexi, ObjectID#equalsaby sobie z tym poradzić, zmieniając sortfunkcję na:
docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) -
ids.findIndex(id => b._id.equals(id)));
Lub z dowolną wersją Node.js, z podkreśleniem / lodash findIndex:
docs.sort(function (a, b) {
return _.findIndex(ids, function (id) { return a._id.equals(id); }) -
_.findIndex(ids, function (id) { return b._id.equals(id); });
});
Document#equalsdo porównania z _idpolem doc . Zaktualizowano, aby _idporównanie było wyraźne. Dzięki, że pytasz.
Podobnie jak w rozwiązaniu JonnyHK , możesz zmienić kolejność dokumentów zwróconych findw swoim kliencie (jeśli twój klient jest w JavaScript) za pomocą kombinacji mapi Array.prototype.findfunkcji w EcmaScript 2015:
Collection.find({ _id: { $in: idArray } }).toArray(function(err, res) {
var orderedResults = idArray.map(function(id) {
return res.find(function(document) {
return document._id.equals(id);
});
});
});
Kilka uwag:
idArrayJest tablicąObjectIdmapwywołaniu zwrotnym, aby uprościć swój kod.findprzechodzi przez tablicę dla każdego elementu tablicy (od zewnętrznej map). Jest to strasznie nieefektywne, ponieważ istnieje rozwiązanie O (n) korzystające z tabeli przeglądowej.
Wiem, że to pytanie jest związane z frameworkiem Mongoose JS, ale zduplikowany jest ogólny, więc mam nadzieję, że opublikowanie tutaj rozwiązania w języku Python (PyMongo) jest w porządku.
things = list(db.things.find({'_id': {'$in': id_array}}))
things.sort(key=lambda thing: id_array.index(thing['_id']))
# things are now sorted according to id_array order
Łatwym sposobem uporządkowania wyniku po zwróceniu tablicy przez mongo jest utworzenie obiektu o identyfikatorze jako kluczy, a następnie odwzorowanie podanych _id w celu zwrócenia tablicy, która jest poprawnie uporządkowana.
async function batchUsers(Users, keys) {
const unorderedUsers = await Users.find({_id: {$in: keys}}).toArray()
let obj = {}
unorderedUsers.forEach(x => obj[x._id]=x)
const ordered = keys.map(key => obj[key])
return ordered
}
Wiem, że to stary wątek, ale jeśli po prostu zwracasz wartość identyfikatora w tablicy, być może będziesz musiał wybrać tę składnię. Ponieważ nie mogłem uzyskać wartości indexOf pasującej do formatu mongo ObjectId.
obj.map = function() {
for(var i = 0; i < inputs.length; i++){
if(this._id.equals(inputs[i])) {
var order = i;
}
}
emit(order, {doc: this});
};
Możesz zagwarantować zamówienie za pomocą $ lub klauzuli.
Więc użyj $or: [ _ids.map(_id => ({_id}))]zamiast tego.
$orobejście nie działa od wersji 2.6 .
Jest to rozwiązanie kodowe po pobraniu wyników z Mongo. Używanie mapy do przechowywania indeksu, a następnie zamiana wartości.
catDetails := make([]CategoryDetail, 0)
err = sess.DB(mdb).C("category").
Find(bson.M{
"_id": bson.M{"$in": path},
"is_active": 1,
"name": bson.M{"$ne": ""},
"url.path": bson.M{"$exists": true, "$ne": ""},
}).
Select(
bson.M{
"is_active": 1,
"name": 1,
"url.path": 1,
}).All(&catDetails)
if err != nil{
return
}
categoryOrderMap := make(map[int]int)
for index, v := range catDetails {
categoryOrderMap[v.Id] = index
}
counter := 0
for i := 0; counter < len(categoryOrderMap); i++ {
if catId := int(path[i].(float64)); catId > 0 {
fmt.Println("cat", catId)
if swapIndex, exists := categoryOrderMap[catId]; exists {
if counter != swapIndex {
catDetails[swapIndex], catDetails[counter] = catDetails[counter], catDetails[swapIndex]
categoryOrderMap[catId] = counter
categoryOrderMap[catDetails[swapIndex].Id] = swapIndex
}
counter++
}
}
}