Edit Page

Aggregations

Aggregations are powerful operations that process data records and return computed results. They allow you to analyze data, perform calculations, and transform documents through a series of stages called a pipeline.

With RESTHeart, you can easily execute MongoDB aggregations through simple REST API calls.

Running Aggregations

To execute an aggregation, send a GET request to:

GET /collection/_aggrs/aggregation-name?avars={"var1":"value1"}

RESTHeart will process the request and return the computed results in the response body.

Tip
By default, aggregation results are returned directly in the response without being written to the database. For persistent results, use Materialized Views.

Defining Aggregations

Aggregations must be defined in the collection’s metadata before they can be used.

Creating an Aggregation

To define an aggregation, use a PATCH request to update the collection metadata:

PATCH /mycollection HTTP/1.1
Content-Type: application/json

{
  "aggrs": [
    {
      "uri": "sales-by-region",
      "stages": [
        { "$match": { "status": "completed" } },
        { "$group": {
            "_id": "$region",
            "total": { "$sum": "$amount" }
          }
        },
        { "$sort": { "total": -1 } }
      ]
    }
  ]
}

This defines an aggregation named sales-by-region that: 1. Filters for completed sales 2. Groups documents by region 3. Calculates the total amount for each region 4. Sorts the results by total in descending order

Aggregation Properties

Property Description Required

uri

The name used in the URL path: /collection/_aggrs/uri

Yes

stages

Array of MongoDB aggregation pipeline stages

Yes

allowDiskUse

Allows operations to use more than 100MB of memory

No (default: false)

Parameterizing Aggregations

Make your aggregations dynamic by using variables that can be passed at runtime.

The $var Operator

Use the $var operator in your aggregation stages to reference variables:

{
  "aggrs": [
    {
      "uri": "sales-by-product",
      "stages": [
        { "$match": { "product": { "$var": "productName" } } },
        { "$group": { "_id": "$month", "sales": { "$sum": "$amount" } } }
      ]
    }
  ]
}

Passing Variables

When executing the aggregation, provide variable values using the avars query parameter:

GET /mycollection/_aggrs/sales-by-product?avars={"productName":"Widget Pro"} HTTP/1.1

This returns sales data only for the "Widget Pro" product.

Default Values

Since RESTHeart 7.3, you can specify default values for variables:

{ "$var": [ "sortField", { "date": -1 } ] }

This uses the provided sortField value if available, otherwise defaults to sorting by date in descending order.

Example with default value:

{
  "aggrs": [
    {
      "uri": "recent-orders",
      "stages": [
        { "$sort": { "$var": [ "sortBy", { "date": -1 } ] } },
        { "$limit": 10 }
      ]
    }
  ]
}

Dot Notation for Nested Variables

Since RESTHeart 7.6, you can access nested properties in variables using dot notation:

GET /mycollection/_aggrs/my-pipeline?avars={"config":{"limit":10,"skip":20}} HTTP/1.1

In the aggregation definition:

{ "$limit": { "$var": "config.limit" } }

This resolves to { "$limit": 10 }.

Predefined Variables

RESTHeart provides several predefined variables that you can use in your aggregations:

Variable Description

@user

The authenticated user object (e.g., @user._id)

@mongoPermissions

The user’s MongoDB permissions (e.g., @mongoPermissions.readFilter)

@page

Current page number from query parameter

@pagesize

Page size from query parameter

@skip

Calculated as (page-1)*pagesize

@limit

Same as @pagesize

Pagination in Aggregations

Unlike regular document queries, pagination in aggregations must be handled explicitly using the $skip and $limit stages.

Use the predefined variables to implement pagination:

{
  "aggrs": [
    {
      "uri": "paginated-results",
      "stages": [
        { "$match": { "active": true } },
        { "$sort": { "lastName": 1 } },
        { "$skip": { "$var": "@skip" } },
        { "$limit": { "$var": "@limit" } }
      ]
    }
  ]
}

Request with pagination:

GET /mycollection/_aggrs/paginated-results?page=3&pagesize=25 HTTP/1.1

This skips the first 50 documents and returns the next 25.

Conditional Stages with $ifvar

Since RESTHeart 7.3, you can include stages conditionally based on whether specific variables are provided.

Basic Conditional Stage

Include a stage only if a variable is provided:

{
  "uri": "conditional-pipeline",
  "stages": [
    { "$match": { "type": "product" } },
    { "$ifvar": [ "category", { "$match": { "category": { "$var": "category" } } } ] }
  ]
}

The second stage only applies if the category variable is provided.

Multiple Required Variables

Require multiple variables for a stage:

{ "$ifvar": [ ["minPrice", "maxPrice"],
  { "$match": {
      "price": {
        "$gte": { "$var": "minPrice" },
        "$lte": { "$var": "maxPrice" }
      }
    }
  }
]}

Else Clause

Provide an alternative stage when variables are missing:

{ "$ifvar": [ "sortBy",
  { "$sort": { "$var": "sortBy" } },
  { "$sort": { "createdAt": -1 } }
]}

This sorts by the provided field if sortBy is given, otherwise sorts by creation date.

Materialized Views

Create persistent collections based on aggregation results using the $merge stage:

{
  "aggrs": [
    {
      "uri": "sales-summary",
      "stages": [
        { "$group": { "_id": "$category", "totalSales": { "$sum": "$amount" } } },
        { "$merge": { "into": "categorySalesSummary" } }
      ]
    }
  ]
}

When this aggregation is executed, results are written to the categorySalesSummary collection:

GET /mycollection/_aggrs/sales-summary HTTP/1.1

The response will be empty, but a new collection is created or updated:

GET /categorySalesSummary HTTP/1.1

HTTP/1.1 200 OK
[
  { "_id": "electronics", "totalSales": 253489.99 },
  { "_id": "furniture", "totalSales": 187245.50 }
]

Incremental Updates

The $merge stage is more efficient than the older $out stage because it can update existing documents rather than replacing the entire collection each time.

Security Considerations

RESTHeart checks variables for MongoDB operators to prevent injection attacks. This protection can be disabled in the configuration file, but this is strongly discouraged.

mongo:
    aggregation-check-operators: true  # Default setting

Transaction Support

Execute aggregations within a transaction by including the sid and txn parameters:

GET /mycollection/_aggrs/my-pipeline?sid=session-id&txn=transaction-id HTTP/1.1

See the Transactions documentation for details.

Example Use Cases

Monthly Sales Report

{
  "uri": "monthly-sales",
  "stages": [
    { "$match": {
        "date": {
          "$gte": { "$var": [ "startDate", { "$date": "2023-01-01T00:00:00Z" } ] },
          "$lte": { "$var": [ "endDate", { "$date": "2023-12-31T23:59:59Z" } ] }
        }
      }
    },
    { "$group": {
        "_id": { "$dateToString": { "format": "%Y-%m", "date": "$date" } },
        "count": { "$sum": 1 },
        "totalAmount": { "$sum": "$amount" }
      }
    },
    { "$sort": { "_id": 1 } }
  ]
}

User Activity Analytics

{
  "uri": "user-activity",
  "stages": [
    { "$match": { "userId": { "$var": "userId" } } },
    { "$group": {
        "_id": "$activityType",
        "count": { "$sum": 1 },
        "lastActivity": { "$max": "$timestamp" }
      }
    },
    { "$sort": { "count": -1 } }
  ]
}

Map-Reduce (Deprecated)

Warning
Map-reduce operations are deprecated in MongoDB. Use aggregation pipelines instead.

For legacy systems, RESTHeart still supports map-reduce:

{
  "type": "mapReduce",
  "uri": "word-count",
  "map": "function() { var words = this.text.split(' '); words.forEach(function(word) { emit(word.toLowerCase(), 1); }); }",
  "reduce": "function(key, values) { return Array.sum(values); }",
  "query": { "type": { "$var": "documentType" } }
}

Variables can be used in both the query and JavaScript functions. In JavaScript functions, use JSON.parse($vars) to access variables:

function() {
  var minLength = JSON.parse($vars).minLength || 0;
  var words = this.text.split(' ');
  words.forEach(function(word) {
    if (word.length > minLength) {
      emit(word.toLowerCase(), 1);
    }
  });
}