Retrieve only queried element in an object array in MongoDB collection

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

Intro

In this "Quick Tip" article, we review how to retrieve only the queried element in an object of arrays in MongoDB.

A typical scenario for document-based NoSQL databases is to store documents that contain an array of objects, such as these:

{
"name": "John",
"cars": [{
"make": "Ford",
"colour": "red"
}, {
"make": "Mercedes",
"colour": "blue"
}]
}
{
"name": "Susan",
"cars": [{
"make": "Fiat",
"colour": "red"
}, {
"make": "BMW",
"colour": "black"
}]
}

Often the above is referred to as denormalisation since we place everything we know about a certain data entity into a single document object.

Each person has a cars property, which is an array of car objects, each having a make and a colour property.

Now, what if we'd like to execute a query and return matched documents where the car's colour is "blue". By running a standard find() query, we get the entire document with all the array items returned:

db.people.find({ 'cars.colour': 'blue' }).pretty();
{
"_id" : ObjectId("5c4ab3bf996018528b1ef283"),
"name" : "John",
"cars" : [
{
"make" : "Ford",
"colour" : "red"
},
{
"make" : "Mercedes",
"colour" : "blue"
}
]
}

Updating the previous query to include projection, we still get both objects instead of only the "blue car" that we are searching for:

db.people.find({ 'cars.colour': 'blue' }, { 'cars.colour': 1 }).pretty();
{
"_id" : ObjectId("5c4ab3bf996018528b1ef283"),
"cars" : [
{
"colour" : "red"
},
{
"colour" : "blue"
}
]
}

This could work for certain situations; however, it may be a requirement that the query only returns a single object from the array - namely, the object that has a matching value (that is the "blue car" object).

To achieve this, we need to introduce and use MongoDB's Aggregation. Think of this aggregation as a pipeline where documents go through multiple stages of transformations - that include grouping values together, adding computed fields or even creating virtual sub-objects.

Let's take a look at how the aggregate query would look like:

db.people.aggregate([
{ $match: { 'cars.colour': 'blue' } },
{
$project: {
cars: {
$filter: {
input: '$cars',
as: 'cars',
cond: { $eq: ['$$cars.colour', 'blue'] },
},
},
_id: 0,
},
},
]);

The first part with the $match is straight forward as it's the same that we did before - we are matching documents where the cars' colour is "blue". After the $match we use $project which allows us to pass documents to the next stage in the pipeline along with specific fields (either existing or computed). We essentially rebuild the cars property, with a certain condition as specified by the cond property for $filter - we want to see only cars that have a blue colour and now we are referencing the input for our aggregate (as per the input field, as well as notice how we use $$cars under cond). The result is going to be exactly what we were after:

{ "cars": [{ "make": "Mercedes", "colour": "blue" }] }

And if you're wondering, how to use this from a Node.js context (or from any other MongoDB SDK for that matter), don't worry it couldn't be simpler. All we need is to take the previously seen query and add to Node.js in the following way:

const MongoClient = require('mongodb').MongoClient;
const util = require('util');

(async function () {
const url = 'mongodb://localhost:27017/your-db';
const dbName = 'your-db';
const client = new MongoClient(url, { useNewUrlParser: true });

try {
await client.connect();
const db = client.db(dbName);
const collection = db.collection('your-collection');
const docs = await collection
.aggregate([
{ $match: { 'cars.colour': 'blue' } },
{
$project: {
cars: {
$filter: {
input: '$cars',
as: 'cars',
cond: { $eq: ['$$cars.colour', 'blue'] },
},
},
_id: 0,
},
},
])
.toArray();
console.log(util.inspect(docs, { showHidden: false, depth: null }));
} catch (err) {
console.log(err.stack);
}

client.close();
})();

That's all folks.