---
title: "Opensearch Aggregate"
method: POST
path: "/opensearch/aggregate"
tags: ["OpenSearch"]
---

# Opensearch Aggregate

`POST /opensearch/aggregate`

## Request

- Base URL: `https://be.graph8.com/api/v1`
- URL: `https://be.graph8.com/api/v1/opensearch/aggregate`
- Auth: HTTP bearer

Generic, allowlist-bounded GROUP BY over a whitelisted mashup index.

Translates ``group_by`` into nested OpenSearch ``terms`` aggregations with
metric sub-aggs, then applies ``having`` / ``sort`` / ``limit`` to the
flattened groups. This is the matrix-walk primitive for TAM design (e.g.
count companies by NAICS x state x employee band in one call).

Guardrails: group_by depth is capped at 3, per-level bucket size tapers by
depth (``_AGG_TERMS_SIZE_BY_DEPTH``), the agg list is capped at 10, and
returned groups at ``limit`` (<=1000). Counts from ``terms`` /
``cardinality`` are APPROXIMATE on a sharded index. Since
``mashup_companies``/``mashup_contacts`` share one contact-grained index,
use ``uniqExact`` on ``COMPANY_ID`` for true unique-company counts. Mashup
aggregations automatically apply the organization's deliverability policy.

## Headers

- `Authorization` string, nullable
- `X-Target-Org-Id` string, nullable

## Request body

- OpenSearchAggregateRequest: Generic, allowlist-bounded GROUP BY over a whitelisted mashup index. Translates to nested OpenSearch ``terms`` aggregations (one level per ``group_by`` field) with metric sub-aggregations. ``having`` / ``sort`` / ``limit`` are applied to the flattened groups server-side. Counts from ``terms`` buckets / ``cardinality`` are APPROXIMATE on a sharded index.
  - `index` 'mashup_contacts' | 'mashup_companies' | 'hem2contact' | 'ip2company', required: Logical index to aggregate. Maps to a whitelisted physical alias.
  - `filters` object: Pre-aggregation equality filters, identical semantics to /opensearch/search. RAW UPPERCASE field names.
  - `group_by` string[], required: 1-3 RAW UPPERCASE keyword fields to group on (e.g. ["COMPANY_NAICS", "COMPANY_STATE"]). Must be keyword-type fields -- the canonical COMPANY_* firmographics are. Capped at 3 dimensions to bound cluster cost. ORDER HIGHEST-CARDINALITY FIRST: deeper dimensions get smaller per-level bucket caps (1000 / 100 / 20), so a low-cardinality field placed last keeps full fidelity while a high-cardinality field placed last may be truncated.
  - `agg` OpenSearchAggSpec[], nullable: Metrics to compute per group (max 10). Defaults to a single `count` when omitted.
    - `function` 'count' | 'uniqExact' | 'sum' | 'avg' | 'min' | 'max': count=doc count per group; uniqExact=cardinality (HLL-approximate); sum/avg/min/max=numeric metric over `field`.
    - `field` string, nullable: RAW UPPERCASE field the metric runs over. Required for every function except `count`. For unique-company counts use uniqExact on COMPANY_ID (the index is contact-grained).
    - `alias` string, nullable: Output key for this metric in each group's `agg` map. Defaults to `function`.
  - `having` object, nullable: Post-aggregation group filter keyed by metric alias, e.g. {"count": {"gte": 50}}. Operators: eq, gte, lte, gt, lt.
  - `sort` object[], nullable: Sort the returned groups by a metric alias, e.g. [{"count": "desc"}]. Applied after `having`.
  - `limit` integer: Max number of groups returned (after having + sort). Hard-capped at 1000.

## Response `200`

Successful Response

- ApiResponseOpenSearchAggregateResponse
  - `data` OpenSearchAggregateResponse, required
    - `index` string, required
    - `groups` OpenSearchAggGroup[]
      - `key` object: The group-by field values for this group, e.g. {"COMPANY_NAICS": "3311", "COMPANY_STATE": "OH"}.
      - `agg` object: Metric values keyed by alias, e.g. {"count": 1247, "unique_domains": 1184}.
    - `total_groups` integer
    - `took_ms` integer
  - `pagination` PaginationMeta
    - `page` integer, required: Current page number (1-indexed)
    - `limit` integer, required: Items per page
    - `total` integer, required: Total number of items
    - `has_next` boolean, required: Whether there are more pages
    - `next_cursor` string, nullable: Opaque cursor for the next page; null when there are no more items. Pass it back as the `cursor` query param to fetch the following page without computing page numbers.
    - `total_available` integer, nullable: Search endpoints only: how many records really match, when known. Can be larger than `total`, which is capped at the 10,000 results that can be paged. For company search this is a distinct-company estimate. Null when unknown; absent outside search.
    - `total_is_lower_bound` boolean, nullable: Search endpoints only: true when more records match than `total` shows (the result set is larger than the 10,000 results that can be paged). Absent outside search.
    - `notes` string[], nullable: Search endpoints only: plain-language notes about the result, e.g. that more records match than can be paged, or that a reversed `between` range was put in order.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/graph8/apis/graph8-developer-api.md) · [All operations](https://skmtc.dev/graph8/apis/graph8-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/graph8/apis/graph8-developer-api/revisions/2e7cfa0ca921?raw)
