ruby-on-railsrubymongodbmongodb-querymongodb-ruby

Ruby Mongo Driver Projection Elemmatch


Following the code in http://www.w3resource.com/mongodb/mongodb-elemmatch-projection-operators.php I have set up a test database using the ruby mongodb driver.

For those following along at home, you first need to install the mongo driver as described at https://docs.mongodb.com/ecosystem/tutorial/ruby-driver-tutorial/#creating-a-client, then run the following commands.

client = Mongo::Client.new([ '127.0.0.1:27017'], :database => 'mydb')

test = client['test']

doc = { "_id" => 1, "batch" =>10452, "tran_details" =>[ { "qty" =>200, "prate" =>50, "mrp" =>70 }, { "qty" =>250, "prate" =>50, "mrp" =>60 }, { "qty" =>190, "prate" =>55, "mrp" =>75 } ] }

test.insert_one(doc)

Insert all of the different docs created in the w3 tutorial.

If you look at example 2 in the w3 tutorial, the translated ruby find is:

test.find({"batch" => 10452}).projection({"tran_details" => {"$elemMatch" => {"prate" => 50, "mrp" => {"$gte" => 70}}}}).to_a

which returns the same result as in the example.

=> [{"_id"=>1, "tran_details"=>[{"qty"=>200, "prate"=>50, "mrp"=>70}]}, {"_id"=>3}, {"_id"=>4}]

My problem is that I would like to constrain the results with the constraints above (mrp gte 70 etc) while also specifying which fields are returned.

For instance, constraining only the tran_details that have a mrp gte 70, but in the results returned only include the prate field (or any subset of the fields).

I can return only the prate field with the query:

test.find({"batch" => 10452}).projection({"tran_details.prate" => 1}).to_a

I would like to combine the effects of the two different projections, but I haven't seen any documentation about how to do that online. If you string the two projections to each other, only the final projection has an effect.


Solution

  • To anyone out there --

    The problem can be solved up to one element by using $elemMatch on projection. However, $elemMatch only returns the first result found. To return only parts of embedded documents multiple layers down that fit certain criteria, you need to use the aggregation framework.

    test.find({
      'tran_details.prate' => { '$gt' => 56 }
    }).projection({
      tran_details: {
        '$elemMatch' => {
          prate: { '$gt' => 56 }
        }
      },
      'tran_details.qty' => 1,
      'tran_details.mrp' => 1,
      batch: 1,
      _id: 0
    }).to_a
    

    To return only parts of embedded documents multiple layers down that fit certain criteria, you need to use the aggregation framework.

    Here is example code

    test.aggregate([{
       '$match': {
          '$or': [
              {'batch': 10452}, {'batch': 73292}]}}, 
       {'$project':
          {trans_details: 
              {'$filter': {
                 input: '$tran_details', 
                 as: 'item', 
                 cond: {'$and':[
                   {'$gte' => ['$$item.prate', 51]},
                   {'gt' =>    ['$$item.prate', 53]}
                 ]}
               }
          }
       }
    }]).to_a
    

    If anyone sees this and knows how to dynamically construct queries in ruby from strings please let me know! Something to do with bson but still trying to find relevant documentation. Thanks -