Filter documents by matching arrays of objects using MongoDB Aggregation

I am dealing with documents that contain arrays of nested objects. Some fields have been omitted for simplicity.

Here is an example of how the documents are structured (2 documents):

{
  title: 'abc',
  parts: [
    {
      part: "verse",
      progressions: [
        {
          progression: "62a4a87da7fdbdabf787e47f",
          key: "Ab",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        },
        {
          progression: "62adf477ed11cbbe156d5769",
          key: "C",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        }
      ],
      _id: "62b5aaa0c9e9fe8a7d7240d2"
    },
    {
      part: "chorus",
      progressions: [
        {
          progression: "62a4a51b4693c43dce9be09c",
          key: "E",
          _id: "62b5aaa0c9e9fe8a7d7240d9"
        }
      ],
      _id: "62b5aaa0c9e9fe8a7d7240d8"
    }
  ]
}

{
  title: 'def',
  parts: [
    {
      part: "verse",
      progressions: [
        {
          progression: "33a4a87da7fopvvbf787erwe",
          key: "E",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        },
        {
          progression: "98opf477ewfscbbe156d5442",
          key: "Bb",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        }
      ],
      _id: "12r3aaa0c4r5me8a7d72oi8u"
    },
    {
      part: "bridge",
      progressions: [
        {
          progression: "62a4a51b4693c43dce9be09c",
          key: "C#",
          _id: "62b5aaa0c9e9fe8a7d7240d9"
        }
      ],
      _id: "62b5aaa0rwfvse8a7d7240d8"
    }
  ]
}

When a client sends a request, they provide an array of objects as parameters:

[
  { part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
  { part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
]

I need to use MongoDB aggregation to retrieve documents that match at least one object in the input array above. For example, documents should have an object in the parts array with "verse" as the part and one of the specified progressions ids in the progressions property. Or documents should have an object in the parts array with "chorus" as the part and a specific progression id. In this case, the first document ('abc') is a match, but there may be more matches in real-world scenarios.

I attempted to create an aggregation pipeline using Mongoose's 'aggregate' method, but it did not work as expected due to my complex approach. I believe I used $elemMatch incorrectly. Any help to improve my aggregation pipeline would be greatly appreciated.

Answer №1

This scenario presents a unique challenge due to the nested arrays that need to be matched, specifically the part and progressions which are on different levels.

An approach that may seem complex at first glance involves creating a new array field named matchCond. This field includes an array called progs containing the parts.progressions. Each sub-object within this array should include the corresponding matching progressions input. Care must be taken to handle cases where there is no matching progressions input array, such as with the "bridge" part in the second document.

The next step is to check if any of these progs items have a progression field that matches an option in the input array. This can be accomplished using the $filter function to reduce the number of results.

The final task is to match the documents that have results and format the answer accordingly.

db.collection.aggregate([
  {
    $set: {
      matchCond: {
        $map: {
          input: "$parts",
          as: "parts",
          in: {progs: {
              $map: {
                input: "$$parts.progressions",
                in: {$mergeObjects: [
                    "$$this",
                    {input: {progressions: []}},
                    {input: {$first: {
                          $filter: {
                            input: inputData,
                            as: "inputPart",
                            cond: {$eq: ["$$inputPart.part", "$$parts.part"]}
                          }
                     }}
                 ]}
              }
          }}
        }
      }
    }
  },
  {$set: {
      matchCond: {
        $reduce: {
          input: "$matchCond",
          initialValue: 0,
          in: {$add: [
              "$$value",
              {$size: {
                  $filter: {
                    input: "$$this.progs",
                    as: "part",
                    cond: {$in: ["$$part.progression", "$$part.input.progressions"]}
                  }
               }
             }
           ]
         }
       }
     }
   },
   {$match: {matchCond: {$gt: 0}}},
   {$project: {title: 1, parts: 1}}
 ])
 

To see this method in action, visit the playground example.

Alternatively, you could consider using $unwind, a simpler but potentially slower option as it duplicates data:

db.collection.aggregate([
   {$addFields: {inputData: inputData, cond: "$parts"}},
   {$unwind: "$cond"},
   {$unwind: "$cond.progressions"},
   {$unwind: "$inputData"},
   {$match: {
       $expr: {
         $and: [
           {$eq: "$cond.part", "$inputData.part"},
           {$in: "$cond.progressions.progression", "$inputData.progressions"}
         ]
       }
     }
   },
   {$project: {title: 1, parts: 1}}
 ])
 

For a demonstration of this approach, visit the playground example - unwind.

There are various options available between these two methods...

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Generating an image element on-the-fly using canvas

Hello, I am looking for a way to manipulate a canvas and use a jQuery plugin that needs an img-element input. Is there a method to dynamically generate an img-element from the canvas using JavaScript? ...

InvalidType: entity does not function as a function

I'm not sure where this issue is originating from, as version 4.0 of CO and version 0.11.14 of Node are installed. The code itself is very simple and worked fine with version 3.1 (just realized the earlier version of co was different) "use strict" co ...

Using V-model binding in Vue always resets the content of text inputs

I am facing an issue with a Text Input that is filled based on a string stored in cookies, similar to a Remember me feature. When I bind the value of the input to the cookie using :value, I am unable to type a new value even if there is no cookie being sto ...

How can LiveValidation be set to trigger validation only upon clicking a specific button?

I am working on an aspx page that includes textboxes for changing passwords. Users are required to enter their current password along with a new one and confirm the new password. The page has buttons for canceling or making changes. When the cancel button ...

Performance delays in sinatra thin application

Encountering strange lag issues with a thin Sinatra app when traffic reaches around 30-40 users. It's a small game utilizing long-polling, resulting in higher http IOs compared to the user count. CPU load remains low and there is plenty of available ...

Issue encountered while attempting to save the date to a json file

While working on my code to directly print the date from index.js into data.json, I encountered an error stating that data is not defined. This is what I have done so far: Ran npm init Installed jsonfile using npm i jsonfile index.js const json ...

connection and navigation hiccup

In my current project, I am utilizing Redux and React. Within App.js, I've implemented some Routes and also make use of the connect function from react-redux. In order to avoid any potential update blocking issues, I typically wrap my component in the ...

What steps should I take to modify this recursive function so that it can verify the property name of an object?

I stumbled upon the code snippet below online, which effectively and recursively eliminates properties from an object if their values are null, undefined, or 0 const removeEmpty = (obj) => { Object.keys(obj).forEach(key => (obj[key] & ...

A guide on calculating the number of days between two dates using Angular 12

I am currently utilizing Angular 12 for my project. The calculation of days based on two dates seems to be working perfectly fine in Chrome, but unfortunately, it fails in Firefox. In my TypeScript file: getDaysCount(firstDate: any, secondDate: any) { ...

PHP Survey and Principles

I am new to PHP and I am attempting to create a poll where each answer holds a value ranging from 0 to 3. My goal is to display a different message based on the total sum of these values when the user submits their responses. For example, if the total sum ...

What is the best way to incorporate NoFollow attributes into the external links on my Weebly website?

It's frustrating that Weebly doesn't offer a way to make external links noFollow. After reaching out to them with no success, I took matters into my own hands and searched for a solution online. <script src='http://ajax.googleapis.com/aj ...

Troubleshooting: JQuery dropdown selection not updating image display

I am trying to create a select menu that changes the car image when a user selects a different model, but for some reason it is not working. Here is what I have tried: <h2 class="model">A6 <img src="images/3.jpg" id="image" width="544" height="2 ...

What is the best way to retrieve all data when the params value is Null, and otherwise match the data?

This function is called when using the get method The goal here is to retrieve data based on the AccountID parameter. If the AccountID is null, then show all data. Otherwise, only return matching data. Where should the condition be placed and how? Client. ...

Transferring information through AJAX and fetching through PHP

Below is my current AJAX code setup: optionScope.data().stage = 'b'; $.ajax({ url: "functions/contact.php", type: "post", data: {'stage': optionScope.data().stage}, success: function(data, status) { ...

Guide to Including Captions and Spans in a Table

In the given HTML code, nested tables are used by the developer which cannot be changed. However, there is a requirement to replace the table class and add a caption and span only to the main table. <table class="two_column_layout" align="center"> & ...

Error: Unable to convert value "proj" to an ObjectId for the "_id" field in the "Student" model

I encountered an error in my route all of a sudden and I'm not sure what's causing it. I am attempting to pass the id from the middleware and use it to retrieve the document. In the field named hostId, I am storing the id of the user who cre ...

Using Puppeteer.js to transfer an array from .addScriptTag to .then

Currently, I am in the process of building a web scraper using puppeteer. I have successfully created a JavaScript script that stores data in an array (working well when tested in the browser console). However, upon attempting to save this data to a JSON f ...

Verify whether the PouchDB database contains any data

Currently, I am developing an application that utilizes PouchDB for local data storage. The app showcases a list of items and my goal is to determine whether the database is empty. This way, if it happens to be empty, I can dynamically add a message ' ...

Uppercase the initial letter in a term to indicate a standard condition

I'm struggling to override the CSS in material UI. Even though I tried capitalizing both words, it only changes on selected and hover states, Is there a way to change it in the normal state as well? I've tried debugging but can't seem to fin ...

Typing a Character Automatically Focuses on Input Field

I have a situation where I want to focus on an input field when the user presses the '/' key. The issue is that once the element is focused, it automatically adds the '/' key to the input field because of how the detection is set up. do ...