0

I have the following structure for a collection in MongoDB

{
   '_id': 45
   'tags': [ 'tag 1', 'tag 3' ]
   'active': true
   'fields': [
      { 'name': 'common field 1', 'type': 'text', 'value': 'some text', ... },
      { 'name': 'common field 2', ... },
      { 'name': 'multivalued field 1', 
        'type': 'multifield',
        'valueCount': 5,
        'value': [
            { 'name': 'subfield1', ..., 'value': [1, 2, 3, 4, 5]},
            { 'name': 'subfield2', ..., 'value': ["one", "two", "three", "four", "five"]},
            { 'name': 'subfield3', ..., 'value': ["here", "there", "", "", ""]}
        ], ... }
   ]
}

and I am trying to implement projection in my API: for example, if the user requests

api/collection/?fields=id,fields{common field 2, multifield{subfield1}} 

The result should be

{
   '_id': 45
   'fields': [
      { 'name': 'common field 1', 'type': 'text', 'value': 'some text', ... },
      { 'name': 'multivalued field 1', 
        'type': 'multifield',
        'valueCount': 5,
        'value': [
            { 'name': 'subfield1', ..., 'value': [1, 2, 3, 4, 5]},
        ], ... }
   ]
}

Since the 'fields' names are not actual keys, I cannot use mongo projection, say

db.collection.find({},{_id: 1, tags: 1, fields.'common field 1': 1})

So I must instead search within the array for the fields whose "name" property matches my projection parameter. I achieved that for the first level array with aggregation and $redact, as suggested in this answer https://stackoverflow.com/a/24032549/5418731

db.points.aggregate([
      { $match: {}},
  {
        $project: {_id :1, fields: 1}
      },
      { $redact : {
       $cond: {
        if: { $or : [{ $not : "$name" }, { $eq: ["$name", "common field 1"] }]},
           then: "$$DESCEND",
           else: "$$PRUNE"
       }
      }}])

However, I cannot use $redact to select subfields from the inner arrays in multivalued fields. The $or parameters would have to be something like

[{ $not : "$name" }, { $eq: ["$name", "common field 1"] }, { $eq: ["$name", "subfield1"] }]

which means a first level field with the same name of the subfield specified would also pass.

After upgrading MongoDB to 3.2, I attempted the $filter solution in this answer https://stackoverflow.com/a/12241930/5418731, which also works fine for the first level array:

db.points.aggregate([
    {$project: {
        fields: {$filter: {
            input: '$fields',
            as: 'field',
            cond: {$eq: ['$$field.name', 'multivalued field 1']}
        }}
    }}
])

but I couldn't find a way to use it "nested" and filter the second level array items. Adding {$eq: ['$$field.value.name', 'subfield1']} doesn't work.

Last, I tried the $map solution presented here https://stackoverflow.com/a/24156418/5418731:

db.points.aggregate([
    { "$project": {
        "_id": 1,
        "fields": {
            "$map": {
                "input": "$fields",
                "as": "f",
                "in": {
                    "$ifNull": [
                        { 
                            "name": "$multivalued field 1",
                            "type": "$multifield", //attempt to restrict search to fields with arrays as values
                            "value": {
                                "$map": {
                                    "input": "$$f.value",
                                    "as": "v",
                                    "in": {
                                        "$ifNull": [
                                            { "name": "$subfield1"},
                                            false
                                        ]
                                    }
                                }
                            }
                        },
                        false
                    ]
                }
            }
        }
    }}
])

But this one won't work because the "value" property of each "fields" item is not necessarily an array, and when it isn't the whole query fails.

I'm about to give up and mask the results in JS. Is there a good solution for that with Mongo?

Community
  • 1
  • 1
Clara Daia
  • 149
  • 1
  • 1
  • 6

1 Answers1

0

Is there a typo in your sample request. As you're querying for documents with fields where name is "common field 2" but you're expecting "common field 1" in your response.

Also instead of making this so complex, you can simply use the aggregation pipeline and proceed in the following manner:

  1. First $unwind on the fields array.
  2. Then $match fields where fields.name = "common field 1" and type = "multifield".
  3. Then $unwind on the value array.
  4. Finally $match the fields where fields.value.name = "subfield1"

Something like this :

db.points.aggregate([
    { $unwind: "$fields" },
    { $match: { "fields.name": "common field 1", "fields.type": "multifield" } },
    { $unwind: "$fields.value" },
    { $match: { "fields.value.name": "subfield1" } }
]);
SiddAjmera
  • 32,111
  • 5
  • 45
  • 85