DEV Community

Mask Databases
Mask Databases

Posted on

Simplifying MongoDB Aggregations with Plain English

MongoDB's aggregation framework is incredibly powerful, allowing you to process and transform documents in complex ways. From filtering and grouping to joining and reshaping data, aggregations are a cornerstone of advanced data retrieval in NoSQL databases. However, writing these pipelines can quickly become verbose and difficult to read, especially as they grow in complexity.

The Challenge of MongoDB Aggregation Pipelines

Let's consider a common scenario: you have a collection of orders and a collection of users. Each order has a userId. You want to find the total number of orders and the total order value for each active user, but only for users who have placed at least one order. You also want to sort the results by the total order value in descending order.

Here's what a typical MongoDB aggregation pipeline for this might look like using the native driver:

const pipeline = [
  // Stage 1: Join orders with users (lookup)
  { $lookup: {
      from: 'users',
      localField: 'userId',
      foreignField: '_id',
      as: 'userDetails'
  }},
  // Stage 2: Unwind the userDetails array (since lookup returns an array)
  { $unwind: '$userDetails' },
  // Stage 3: Filter for active users
  { $match: {
      'userDetails.status': 'active'
  }},
  // Stage 4: Group by user and calculate aggregates
  { $group: {
      _id: '$userId',
      userName: { $first: '$userDetails.name' },
      totalOrders: { $sum: 1 },
      totalOrderValue: { $sum: '$value' }
  }},
  // Stage 5: Filter out users with no orders (though $match on userDetails.status might handle this implicitly if no orders means no userDetails)
  { $match: {
      totalOrders: { $gt: 0 }
  }},
  // Stage 6: Sort by totalOrderValue descending
  { $sort: { totalOrderValue: -1 } }
];

const result = await db.collection('orders').aggregate(pipeline).toArray();
console.log(result);
Enter fullscreen mode Exit fullscreen mode

Even for a moderately complex query, this pipeline involves multiple stages ($lookup, $unwind, $match, $group, $sort), each with its own syntax and structure. Debugging can be tricky, and understanding the intent just from reading the operators requires a solid grasp of the aggregation framework.

Decoupling Intent from Implementation

The core issue here is that the intent of the query ("get active users' order summaries, sorted") is tightly coupled with the implementation details (specific aggregation operators, field names, and their order). When requirements change, or when new team members onboard, deciphering and modifying these pipelines can be a significant time sink.

An alternative approach is to describe your data models and your query intent in a more human-readable format. Imagine if you could simply state what you want, and a system handles the translation into the correct database operations.

For example, first, you'd define your models in plain English, giving the system context about your collections and their relationships. For instance:

const { MaskModels } = require('mask-databases');

MaskModels.define(
  'Users. Collection users. People who sign into the app. Their full name, the ' +
  'email they log in with (two people must not share the same email), and whether ' +
  'the account is active or turned off.'
);

MaskModels.define(
  'Orders. Collection orders. Each order has a unique ID, belongs to one customer, ' +
  'and has a total monetary value. An order can have multiple items, but for this ' +
  'example, we only care about the total value.'
);
Enter fullscreen mode Exit fullscreen mode

Then, for the query, you would describe your intent in natural language:

const { MaskDatabase } = require('mask-databases');

const usersWithOrderSummaries = await MaskDatabase.prompt(
  'get active users who have placed orders, showing their name, total number of orders, ' +
  'and total order value. Sort the results by total order value, highest first.'
);

console.log(usersWithOrderSummaries);
Enter fullscreen mode Exit fullscreen mode

This approach shifts the focus from how to construct the pipeline to what data you need. The natural language description is then compiled into the exact MongoDB aggregation pipeline, ensuring it's deterministic and matches your defined models. This compilation happens once, ahead of time, meaning there are zero AI calls at runtime, keeping your application fast and predictable. This kind of system also makes your query logic readable, almost like documentation, which can greatly ease review, onboarding, and debugging for backend teams.

If you're interested in exploring how natural language can streamline your database interactions, including complex aggregations, you can try out the Mask Databases playground without any signup required at https://maskdatabases.com/playground.

Top comments (0)