4

Here is a doc I have:

var docIHave = {
    _id: "someId",
    things: [
        {
            name: "thing1",
            stuff: [1,2,3,4,5,6,7,8,9]
        },
        {
            name: "thing2",
            stuff: [4,5,6,7,8,9,10,11,12,13,14]
        },
        {
            name: "thing3",
            stuff: [1,4,6,8,11,21,23,30]
        }
    ]
}

This is the doc I want:

var docIWant = {
    _id: "someId",
    things: [
        {
            name: "thing1",
            stuff: [5,6,7,8,9]
        },
        {
            name: "thing2",
            stuff: [5,6,7,8,9,10,11]
        },
        {
            name: "thing3",
            stuff: [6,8,11]
        }
    ]
}

stuff´s of docIWant should only contain items greater than min=4 and smaller than max=12.

Background: I have a meteor app and I subscribe to a collection giving me docIHave. Based on parameters min and max I need the docIWant "on the fly". The original document should not be modified. I need a query or procedure that returns me docIWant with the subset of stuff.

A practical code example would be greatly appreciated.

4
  • try to use $elemMatch Commented May 27, 2015 at 10:04
  • Is this logic only needed for display purposes? If so, could the docs be trnsformed prior to sending them to the template? Alternatively, could you just conditionally not show some of the data in your template? I'm happy to give a solution for either if this sounds like it's on the right track. Commented May 27, 2015 at 14:02
  • @DavidWeldon Yes, actually its only needed for display purposes. If they were to be transformed, that would imply, that they have to be sent to the client everytime I change min or max, right? In my usecase I am relying on the length of the stuff array in the template to display the count of the items. But maybe I could pass it through a meteor helper to remove unwanted items. Commented May 27, 2015 at 16:12
  • There's no need to modify your publisher. You can just transform the data on the client before it gets to the template, or as you suggested, you could transform each array with a helper. I added an answer showing the former case. It assumes a single document, but could easily be modified for an cursor/array. Let me know if that works for you. Commented May 27, 2015 at 17:16

3 Answers 3

2

Use the aggregation framework for this. In the aggregation pipeline, consider the $match operator as your first pipeline stage. This is quite necessary to optimize your aggregation as you would need to filter documents that match the given criteria first before passing them on further down the pipeline.

Next use the $unwind operator. This deconstructs the things array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.

Another $unwind operation would be needed on the things.stuff array as well.

The next pipeline stage would then filter dopcuments where the deconstructed things.stuff match the given min and max criteria. Use a $match operator for this.

A $group operator is then required to group the input documents by a specified identifier expression and applies the accumulator expression $push to each group. This creates an array expression to each group.

Typically your aggregation should end up like this (although I haven't actually tested it but this should get you going in the right direction):

db.collection.aggregate([
    {
        "$match": {
            "things.stuff": { "$gt": 4, "$lte": 11 }
        }
    },
    {
        "$unwind": "$things"
    },
    {
        "$unwind": "$things.stuff"
    },
    {
        "$match": {
            "things.stuff": { "$gt": 4, "$lte": 11 }
        }
    },
    {
        "$group": {
            "_id": {
                "_id": "$_id",
                "things": "$things"
            },
            "stuff": {
                "$push": "$things.stuff"
            }
        }
    },
    {
        "$group": {
            "_id": "$_id._id",  
            "things": {
                "$push": {
                    "name": "$_id.things.name",
                    "stuff": "$stuff"
                }
            }
        }
    }
])
Sign up to request clarification or add additional context in comments.

3 Comments

why you used two times match, to match same things ?
@yogesh I've explained that bit in the first paragraph of the answer but basically, the first $match is necessary for reducing the amount of data passing through the pipeline.
This might be useful if you want to publish/subscribe to this query: stackoverflow.com/questions/30278327/…
2

If you need to transform the document on the client for display purposes, you could do something like this:

Template.myTemplate.helpers({
  transformedDoc: function() {
    // get the bounds - maybe these are stored in session vars
    var min = Session.get('min');
    var max = Session.get('max');

    // fetch the doc somehow that needs to be transformed
    var doc = SomeCollection.findOne();

    // transform the thing.stuff arrays
    _.each(doc.things, function(thing) {
      thing.stuff = _.reject(thing.stuff, function(n) {
        return (n < min) || (n > max);
      });
    });

    // return the transformed doc
    return doc;
  }
});

Then in your template: {{#each transformedDoc.things}}...{{/each}}

2 Comments

Thanks so much. Running the document through a helper really did the trick way easier than I had in mind with the querying of the document containing a subset. But I accepted Vishwas answer because it answered my actual question. Your blog post about model layers also was really helpful :)
Cool. I'm glad you got it working, and that you liked the blog post. :D
1

Use mongo aggregation like following : First use $unwind this will unwind stuff and then use $match to find elements greater than 4. After that $group data based on things.name and add required fields in $project.

The query will be as following:

db.collection.aggregate([
{
$unwind: "$things"
}, {
$unwind: "$things.stuff"
}, {
$match: {
    "things.stuff": {
        $gt: 4,
        $lt:12
    }
}
}, {
$group: {
    "_id": "$things.name",
    "stuff": {
        $push: "$things.stuff"
    }
}
}, {
$project: {
    "thingName": "$_id",
    "stuff": 1
}
}])

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.