Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB find value in list of lists

I am using MongoDB to search for elements that contain a list of lists where at least one item in the list matches the search parameter.

This is an example of the structure I currently have.

{  
   "Item 1":{  
      "data":[  
         ["green", 1]
      ]
   },
   "Item 2":{  
      "data":[  
         ["blue", 9],
         ["green", 1]
      ]
   }
}

I want to search for all items that have the value "green" in the data list.

I currently have this:

my_data.find({'data': {'$in': ['green']}})

however, no results are returned.

like image 268
Gunther Avatar asked Oct 17 '15 18:10

Gunther


People also ask

How do I search a list in MongoDB?

To search the array of object in MongoDB, you can use $elemMatch operator. This operator allows us to search for more than one component from an array object.

How do I search multiple values in MongoDB?

MongoDB provides the find() that is used to find multiple values or documents from the collection. The find() method returns a cursor of the result set and prints all the documents. To find the multiple values, we can use the aggregation operations that are provided by MongoDB itself.

How do I find a specific value in MongoDB?

Find() Method. In MongoDB, find() method is used to select documents in a collection and return a cursor to the selected documents. Cursor means a pointer that points to a document, when we use find() method it returns a pointer on the selected documents and returns one by one.

How do I use $in in MongoDB?

This operator is used to select those documents where the value of the field is equal to any of the given value in the array. And if the field contains an array, then this operator selects only those documents whose field contains an array that holds at least one item that matches a value of the specified array.


1 Answers

You need a "direct path" to any queried elements and "data" is not the "top level" of the path that you need to search here, but rather it is "Item 1" or "Item 2" within the document and "data" is a sub-element of that.

The basic case is to use "dot notation" where the root elements are "known" to at least possibly exist, and also note that the $in operator is not necessary just to match a value within an array, since MongoDB does not care. But you will need a nested $elemMatch instead:

db.my_data.find({
    "$or": [
        { "Item 1.data": { 
            "$elemMatch": { 
                "$elemMatch": { "$eq": "green" }
            }
        }},
        { "Item 2.data": {
            "$elemMatch": {
                "$elemMatch": { "$eq": "green" }
            }
        }}
    ]
})

There are no "wildcards" for matching a part of a preceeding path so it is necessary to state the complete path within an $or condition to search each possible path in the document.

If writing out all possible prefixed keys is not practical, then your only other approach is using JavaScript evaluation with $where to determine the logic for matching:

db.my_data.find(function() {
    var doc = this;
    delete doc._id;

    var matched = false;
    for ( k in doc ) {
        matched = doc[k].data.some(function(el) { 
            return el.some(function(inner) {
                return inner === "green";
            });
        });
        if (matched) break;
    }
    return matched;
})

Or some variation of that logic to actually test possible elements with "data" arrays to contain your "green" value. It's not a great solution since $where requires execution on each document in order to evaluate the condition and therefore cannot use an index to filter results. This means scanning the whole collection and is basically the slowest approach.

Also just showing the "shell shortcut" to writing that as otherwise the function() contents is just submitted as a "string" in argument to $where for pymongo or other language drivers, but the basic context is that it is JavaScript code that is evaluated on the server.

Better yet is to change your document so there is always a consistent path to the element you want to query. Such as:

{
    "items": [
        { "name": "Item 1", "data": [["green", 1]] },
        { "name": "Item 2", "data": [["blue", 9], ["green", 1]] }
    ]  
}

Then you can just issue this simple query:

 db.my_data.find({ "items.data": { "$elemMatch": { "$elemMatch": { "$eq": "green" } } } })

As the "path" is always "items.data" this allows one single condtion just for that path to test all elements

like image 113
Blakes Seven Avatar answered Sep 28 '22 17:09

Blakes Seven