MongoDB's $ in clause รับประกันคำสั่งซื้อหรือไม่


90

เมื่อใช้$inประโยคของ MongoDB ลำดับของเอกสารที่ส่งคืนจะสอดคล้องกับลำดับของอาร์กิวเมนต์อาร์เรย์หรือไม่


8
ตั๋ว MongoDBสำหรับคุณสมบัตินี้
Mitar

คำตอบ:


80

ตามที่ระบุไว้ลำดับของอาร์กิวเมนต์ในอาร์เรย์ของ $ ในอนุประโยคไม่ได้แสดงถึงลำดับวิธีการดึงเอกสาร แน่นอนว่าจะเป็นลำดับธรรมชาติหรือตามลำดับดัชนีที่เลือกดังที่แสดง

หากคุณต้องการรักษาคำสั่งซื้อนี้โดยพื้นฐานแล้วคุณมีสองตัวเลือก

ดังนั้นขอบอกว่าคุณถูกจับคู่กับค่าของ_idในเอกสารของคุณด้วยอาร์เรย์ที่กำลังจะถูกส่งผ่านไปยังเป็น$in[ 4, 2, 8 ]

วิธีการโดยใช้ Aggregate


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 } }

])

นั่นจะเป็นรูปแบบขยาย สิ่งที่เกิดขึ้นโดยทั่วไปที่นี่ก็คือเมื่ออาร์เรย์ของค่าถูกส่งไป$inยังคุณก็สร้าง "ซ้อน"$condคำสั่งเพื่อทดสอบค่าและกำหนดน้ำหนักที่เหมาะสม เนื่องจากค่า "น้ำหนัก" นั้นสะท้อนถึงลำดับขององค์ประกอบในอาร์เรย์คุณจึงสามารถส่งผ่านค่านั้นไปยังขั้นการจัดเรียงเพื่อให้ได้ผลลัพธ์ตามลำดับที่ต้องการ

แน่นอนคุณ "สร้าง" คำสั่งไปป์ไลน์ในโค้ดเช่นนี้:

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 );

วิธีการโดยใช้ mapReduce


แน่นอนว่าถ้าทุกอย่างดูเหมือนจะหนักสำหรับความอ่อนไหวของคุณคุณสามารถทำสิ่งเดียวกันโดยใช้ mapReduce ซึ่งดูง่ายกว่า แต่มีแนวโน้มว่าจะทำงานช้าลงบ้าง

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;
        }
    }
)

และโดยพื้นฐานแล้วจะอาศัยค่า "คีย์" ที่ปล่อยออกมาซึ่งอยู่ใน "ลำดับดัชนี" ของการเกิดขึ้นในอาร์เรย์อินพุต


ดังนั้นโดยพื้นฐานแล้วนั่นคือวิธีการของคุณในการรักษาลำดับของรายการอินพุตไปยัง$inเงื่อนไขที่คุณมีรายการนั้นตามลำดับที่กำหนด


2
คำตอบที่ดี สำหรับผู้ที่ต้องการเวอร์ชั่นกาแฟที่นี่
Lawrence Jones

1
@NeilLunn ฉันพยายามใช้วิธีการรวม แต่ฉันได้รับรหัสและน้ำหนัก คุณรู้วิธีดึงโพสต์ (วัตถุ) หรือไม่?
Juanjo Lainez Reche

1
@NeilLunn ฉันทำจริง (อยู่ที่นี่stackoverflow.com/questions/27525235/… ) แต่ความคิดเห็นเดียวที่อ้างถึงที่นี่แม้ว่าฉันจะตรวจสอบสิ่งนี้ก่อนโพสต์คำถามของฉัน คุณสามารถช่วยฉันที่นั่นได้ไหม ขอขอบคุณ!
Juanjo Lainez Reche

1
รู้ว่านี่เก่า แต่ฉันเสียเวลาไปมากในการแก้ไขจุดบกพร่องว่าทำไม inputs.indexOf () ไม่ตรงกับ this._id หากคุณเพียงแค่คืนค่าของรหัสวัตถุคุณอาจต้องเลือกใช้ไวยากรณ์นี้: obj.map = function () {สำหรับ (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (อินพุต [i])) {var order = i; }} ปล่อย (คำสั่ง, {doc: this}); };
NoobSter

1
คุณสามารถใช้ "$ addFields" แทน "$ project" ได้หากคุณต้องการมีฟิลด์ดั้งเดิมทั้งหมดด้วย
Jodo

40

อีกวิธีหนึ่งในการใช้แบบสอบถาม Aggregation ใช้ได้กับMongoDB verion> = 3.4 -

เครดิตไปที่โพสต์บล็อกที่ดีนี้

ตัวอย่างเอกสารที่จะดึงตามลำดับนี้ -

var order = [ "David", "Charlie", "Tess" ];

คำถาม -

var query = [
             {$match: {name: {$in: order}}},
             {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
             {$sort: {"__order": 1}}
            ];

var result = db.users.aggregate(query);

คำพูดอื่นจากโพสต์ที่อธิบายตัวดำเนินการรวมเหล่านี้ที่ใช้ -

ขั้นตอน "$ addFields" เป็นขั้นตอนใหม่ใน 3.4 และช่วยให้คุณสามารถ "$ project" ฟิลด์ใหม่ไปยังเอกสารที่มีอยู่โดยไม่ต้องรู้ฟิลด์อื่น ๆ ที่มีอยู่ทั้งหมด นิพจน์ "$ indexOfArray" ใหม่จะส่งคืนตำแหน่งขององค์ประกอบเฉพาะในอาร์เรย์ที่กำหนด

โดยทั่วไปตัวaddFieldsดำเนินการจะต่อท้ายorderฟิลด์ใหม่ในทุกเอกสารเมื่อพบและorderฟิลด์นี้แสดงถึงลำดับเดิมของอาร์เรย์ของเราที่เราให้ไว้ จากนั้นเราก็จัดเรียงเอกสารตามฟิลด์นี้


มีวิธีการจัดเก็บอาร์เรย์คำสั่งเป็นตัวแปรในการสืบค้นหรือไม่ดังนั้นเราจึงไม่มีการสืบค้นอาร์เรย์จำนวนมากนี้ซ้ำสองครั้งหากอาร์เรย์มีขนาดใหญ่
Ethan SK

27

หากคุณไม่ต้องการใช้aggregateวิธีแก้ปัญหาอื่นคือใช้findแล้วเรียงลำดับ doc ผลลัพธ์ฝั่งไคลเอ็นต์โดยใช้array#sort:

หาก$inค่าเป็นประเภทดั้งเดิมเช่นตัวเลขคุณสามารถใช้วิธีการเช่น:

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);
    });
});

หาก$inค่าเป็นชนิดที่ไม่ใช่พื้นฐานเช่นObjectIds จำเป็นต้องใช้วิธีอื่นเพื่อindexOfเปรียบเทียบโดยการอ้างอิงในกรณีนั้น

หากคุณใช้ Node.js 4.x + คุณสามารถใช้Array#findIndexและObjectID#equalsจัดการสิ่งนี้ได้โดยเปลี่ยนsortฟังก์ชันเป็น:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));

หรือกับ Node.js เวอร์ชันใดก็ได้โดยมีขีดล่าง / 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); });
});

ฟังก์ชั่นเท่ากันรู้ได้อย่างไรว่าจะเปรียบเทียบคุณสมบัติ id กับ id 'return a.equals (id);' ทำให้มีการระงับคุณสมบัติทั้งหมดที่ส่งคืนสำหรับโมเดลนั้น
lboyel

1
@lboyel ฉันไม่ได้หมายความว่ามันจะฉลาดขนาดนั้น :-) แต่มันได้ผลเพราะมันใช้พังพอนDocument#equalsเพื่อเปรียบเทียบกับ_idฟิลด์ของหมอ อัปเดตเพื่อให้การ_idเปรียบเทียบมีความชัดเจน ขอบคุณที่ถาม.
JohnnyHK

6

เช่นเดียวกับโซลูชันของJonnyHKคุณสามารถจัดลำดับเอกสารที่ส่งคืนจากfindในไคลเอนต์ของคุณใหม่ได้ (หากไคลเอนต์ของคุณอยู่ใน JavaScript) ด้วยการรวมกันของmapและArray.prototype.findฟังก์ชันใน 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);
        });
    });

});

หมายเหตุสองสามข้อ:

  • โค้ดด้านบนใช้ไดรเวอร์ Mongo Node ไม่ใช่ Mongoose
  • idArrayเป็นอาร์เรย์ของObjectId
  • ฉันไม่ได้ทดสอบประสิทธิภาพของวิธีนี้เทียบกับการเรียงลำดับ แต่ถ้าคุณต้องการจัดการรายการที่ส่งคืนแต่ละรายการ (ซึ่งเป็นเรื่องธรรมดา) คุณสามารถทำได้ในการmapติดต่อกลับเพื่อลดความซับซ้อนของรหัส

เวลาทำงานคือ O (n * n) เนื่องจากด้านในจะfindข้ามอาร์เรย์สำหรับแต่ละองค์ประกอบของอาร์เรย์ (จากด้านนอกmap) สิ่งนี้ไม่มีประสิทธิภาพอย่างมากเนื่องจากมี O (n) วิธีแก้ปัญหาโดยใช้ตารางการค้นหา
เคอร์แรน

5

ฉันรู้ว่าคำถามนี้เกี่ยวข้องกับเฟรมเวิร์ก Mongoose JS แต่คำถามที่ซ้ำกันนั้นเป็นแบบทั่วไปดังนั้นฉันหวังว่าการโพสต์โซลูชัน Python (PyMongo) จะดีที่นี่

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

5

วิธีง่ายๆในการเรียงลำดับผลลัพธ์หลังจากที่ mongo ส่งคืนอาร์เรย์คือการสร้างอ็อบเจกต์ที่มี id เป็นคีย์จากนั้นแมปบน _id ที่กำหนดเพื่อส่งคืนอาร์เรย์ที่เรียงลำดับอย่างถูกต้อง

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
}

1
สิ่งนี้ทำในสิ่งที่ฉันต้องการและง่ายกว่าความคิดเห็นด้านบนมาก
dyarbrough

@dyarbrough วิธีนี้ใช้ได้กับการสืบค้นที่ดึงเอกสารทั้งหมดเท่านั้น (โดยไม่ จำกัด หรือข้ามไป) ความคิดเห็นด้านบนซับซ้อนกว่า แต่ใช้ได้กับทุกสถานการณ์
marian2js

3

เสมอ? ไม่เลย ลำดับจะเหมือนกันเสมอ: ไม่ได้กำหนด (อาจเป็นลำดับทางกายภาพที่จัดเก็บเอกสาร) เว้นแต่คุณจะจัดเรียง


$naturalสั่งซื้อตามปกติซึ่งเป็นตรรกะมากกว่าทางกายภาพ
Sammaye

1

ฉันรู้ว่านี่เป็นเธรดเก่า แต่ถ้าคุณเพิ่งคืนค่าของ Id ในอาร์เรย์คุณอาจต้องเลือกใช้ไวยากรณ์นี้ เนื่องจากฉันไม่สามารถรับค่า indexOf เพื่อจับคู่กับรูปแบบ 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});
  };

วิธีการแปลง mongo ObjectId .toString โดยไม่รวมกระดาษห่อ 'ObjectId ()' - เพียงแค่ค่า?



0

นี่คือวิธีแก้รหัสหลังจากที่ดึงผลลัพธ์จาก Mongo การใช้แผนที่เพื่อจัดเก็บดัชนีแล้วแลกเปลี่ยนค่า

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++
        }
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.