Question

The objects in the database have a 'properties' array, that can hold diverse objects. Some of them present numbers or ranges and look like:

{'value': 10}

or

{'minValue': 4, 'maxValue': 8}

When querying the collection for a specific number, like X, I want to find all documents where either value equals X or minValue <= X <= maxValue. My first attempt of a query looks like

db.Pool0.find({$or: [{'properties.value': X}, {'properties.minValue': {$lte: X}, 'properties.maxValue': {$gte: X}}]}, {'_id': 1}).pretty()

The drawback is that if the properties array holds multiple objects specifying minValue and maxValue, X can be between any of them. For example

"properties" : [
    {
        "minValue" : 4,
        "maxValue" : 6
    },
    {
        "minValue" : 10,
        "maxValue" : 20
    }
]

will match for X = 8. Can the query be improved so the object structure within properties is respected?

Was it helpful?

Solution

You basically want to use $elemMatch combined with an $or condition in this form:

db.collection.find({
    "$or": [
        {
            "properties": { "$elemMatch": {
                "minValue": { "$lte": 8 },
                "maxValue": { "$gte": 8 }
            }}
        },
        {
            "properties.value": 8
        }
    ]
})

That covers meeting the documents that contain the range as well as the possible other key name for the field in properties.

But keep in mind that matching documents is different to matching elements in an array. So if you just expect the array elements that match to be returned and you have more than one, then you use the aggregate form:

db.collection.aggregate([
    // Matching documents is still good practice
    { "$match": {
        "$or": [
             {
                 "properties": { "$elemMatch": {
                    "minValue": { "$lte": 8 },
                    "maxValue": { "$gte": 8 }
                 }}
             },
             {
                 "properties.value": 8
             }
         ]
    }},

    // Unwind to de-normalize
    { "$unwind": "$properties" },

    // Then match to filter the elements
    { "$match": {
        "$or": [
             {
                "properties.minValue": { "$lte": 8 },
                "properties.maxValue": { "$gte": 8 }
             },
             { "properties.value": 8 }
        ]
    }},

    // Reconstruct the filtered array
    { "$group": {
        "_id": "$_id",
        "properties": { "$push": "$properties" }
    }}
])

But if there will only be one match that you are sure of then simply use projection with find:

db.collection.find(
    { 
        "$or": [
            {
                "properties": { "$elemMatch": {
                    "minValue": { "$lte": 8 },
                    "maxValue": { "$gte": 8 }
                }}
            },
            {
            "properties.value": 8
            }
        ]
    },
    { "properties.$": 1 }
)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top