Skip to content

Commit 9feeca3

Browse files
author
Phil Varner
authored
Merge pull request #439 from stac-utils/pv/aggregations
aggregations endpoint and collection-level aggregate
2 parents 07d1add + 5ac15d5 commit 9feeca3

File tree

4 files changed

+298
-46
lines changed

4 files changed

+298
-46
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8-
## Unreleased - TBD
8+
## [Unreleased] - TBD
99

1010
### Changed
1111

1212
- Updated example serverless configuration to use OpenSearch 2.5.
1313

14+
## Added
15+
16+
- Added support for `/aggregations`, `/collections/{collectionId}/aggregations`, and
17+
`/collections/{collectionId}/aggregate` endpoints.
18+
1419
## [0.8.1] - 2023-03-29
1520

1621
### Added

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- [Locking down transaction endpoints](#locking-down-transaction-endpoints)
2929
- [AWS WAF Rule Conflicts](#aws-waf-rule-conflicts)
3030
- [Queryables](#queryables)
31+
- [Aggregation](#aggregation)
3132
- [Ingesting Data](#ingesting-data)
3233
- [Ingesting large items](#ingesting-large-items)
3334
- [Subscribing to SNS Topics](#subscribing-to-sns-topics)
@@ -835,6 +836,73 @@ from the Collection entity whenever that is returned.
835836
A non-configurable root-level queryables definition is defined with no named terms but
836837
`additionalProperties` set to `true`.
837838

839+
## Aggregation
840+
841+
STAC API supports the [Aggregation Extension](https://github.com/stac-api-extensions/aggregation). This allows the definition of per-collection aggregations that can be
842+
calculated, dependent on the relevant fields being available in the STAC Items in that
843+
Collection. A field named `aggregations` should be added to the Collection object for
844+
the collection for which the aggregations are available, e.g.:
845+
846+
```text
847+
"aggregations": [
848+
{
849+
"name": "total_count",
850+
"data_type": "integer"
851+
},
852+
{
853+
"name": "datetime_max",
854+
"data_type": "datetime"
855+
},
856+
{
857+
"name": "datetime_min",
858+
"data_type": "datetime"
859+
},
860+
{
861+
"name": "datetime_frequency",
862+
"data_type": "frequency_distribution",
863+
"frequency_distribution_data_type": "datetime"
864+
},
865+
{
866+
"name": "grid_code_frequency",
867+
"data_type": "frequency_distribution",
868+
"frequency_distribution_data_type": "string"
869+
},
870+
{
871+
"name": "grid_geohex_frequency",
872+
"data_type": "frequency_distribution",
873+
"frequency_distribution_data_type": "string"
874+
},
875+
{
876+
"name": "grid_geohash_frequency",
877+
"data_type": "frequency_distribution",
878+
"frequency_distribution_data_type": "string"
879+
},
880+
{
881+
"name": "grid_geotile_frequency",
882+
"data_type": "frequency_distribution",
883+
"frequency_distribution_data_type": "string"
884+
}
885+
]
886+
```
887+
888+
Available aggregations are:
889+
890+
- total_count (count of total items)
891+
- collection_frequency (Item `collection` field)
892+
- platform_frequency (Item.Properties.platform)
893+
- cloud_cover_frequency (Item.Properties.eo:cloud_cover)
894+
- datetime_frequency (Item.Properties.datetime, monthly interval)
895+
- datetime_min (earliest Item.Properties.datetime)
896+
- datetime_max (latest Item.Properties.datetime)
897+
- grid_code_frequency (Item.Properties.grid:code)
898+
- grid_code_landsat_frequency (synthesized from Item.Properties.landsat:wrs_path and Item.Properties.landsat:wrs_row)
899+
- sun_elevation_frequency (Item.Properties.view:sun_elevation)
900+
- sun_azimuth_frequency (Item.Properties.view:sun_azimuth)
901+
- off_nadir_frequency (Item.Properties.view:off_nadir)
902+
- grid_geohex_frequency ([GeoHex grid](https://opensearch.org/docs/2.4/opensearch/geohexgrid-agg/) on Item.Properties.proj:centroid)
903+
- grid_geohash_frequency ([geohash grid](https://opensearch.org/docs/2.4/opensearch/bucket-agg/#geo_distance-geohash_grid) on Item.Properties.proj:centroid)
904+
- grid_geotile_frequency (geotile on Item.Properties.proj:centroid)
905+
838906
## Ingesting Data
839907

840908
STAC Collections and Items are ingested by the `ingest` Lambda function, however this Lambda is not invoked directly by a user, it consumes records from the `stac-server-<stage>-queue` SQS. To add STAC Items or Collections to the queue, publish them to the SNS Topic `stac-server-<stage>-ingest`.

src/lambdas/api/app.js

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ app.get('/conformance', async (_req, res, next) => {
7777
app.get('/queryables', async (req, res, next) => {
7878
try {
7979
res.type('application/schema+json')
80-
res.json(await api.getQueryables(req.endpoint))
80+
res.json(await api.getGlobalQueryables(req.endpoint))
8181
} catch (error) {
8282
next(error)
8383
}
@@ -111,7 +111,7 @@ app.post('/search', async (req, res, next) => {
111111

112112
app.get('/aggregate', async (req, res, next) => {
113113
try {
114-
res.json(await api.aggregate(req.query, database, req.endpoint, 'GET'))
114+
res.json(await api.aggregate(null, req.query, database, req.endpoint, 'GET'))
115115
} catch (error) {
116116
if (error instanceof ValidationError) {
117117
next(createError(400, error.message))
@@ -133,6 +133,14 @@ app.post('/aggregate', async (req, res, next) => {
133133
}
134134
})
135135

136+
app.get('/aggregations', async (req, res, next) => {
137+
try {
138+
res.json(await api.getGlobalAggregations(req.endpoint))
139+
} catch (error) {
140+
next(error)
141+
}
142+
})
143+
136144
app.get('/collections', async (req, res, next) => {
137145
try {
138146
res.json(await api.getCollections(database, req.endpoint))
@@ -166,30 +174,52 @@ app.get('/collections/:collectionId', async (req, res, next) => {
166174
const { collectionId } = req.params
167175
try {
168176
const response = await api.getCollection(collectionId, database, req.endpoint)
169-
delete response.queryables
170177
if (response instanceof Error) next(createError(404))
171178
else res.json(response)
172179
} catch (error) {
173180
next(error)
174181
}
175182
})
176183

177-
app.get('/collections/:collectionId/items', async (req, res, next) => {
184+
app.get('/collections/:collectionId/queryables', async (req, res, next) => {
185+
const { collectionId } = req.params
186+
try {
187+
const queryables = await api.getCollectionQueryables(collectionId, database, req.endpoint)
188+
189+
if (queryables instanceof Error) next(createError(404))
190+
191+
res.type('application/schema+json')
192+
res.json(queryables)
193+
} catch (error) {
194+
if (error instanceof ValidationError) {
195+
next(createError(400, error.message))
196+
} else {
197+
next(error)
198+
}
199+
}
200+
})
201+
202+
app.get('/collections/:collectionId/aggregations', async (req, res, next) => {
203+
const { collectionId } = req.params
204+
try {
205+
res.json(await api.getCollectionAggregations(collectionId, database, req.endpoint))
206+
} catch (error) {
207+
if (error instanceof ValidationError) {
208+
next(createError(400, error.message))
209+
} else {
210+
next(error)
211+
}
212+
}
213+
})
214+
215+
const collectionAggregate = async (req, res, next, httpMethod) => {
178216
const { collectionId } = req.params
179217
try {
180218
const response = await api.getCollection(collectionId, database, req.endpoint)
181219

182220
if (response instanceof Error) next(createError(404))
183221
else {
184-
const items = await api.searchItems(
185-
collectionId,
186-
req.query,
187-
database,
188-
req.endpoint,
189-
'GET'
190-
)
191-
res.type('application/geo+json')
192-
res.json(items)
222+
res.json(await api.aggregate(collectionId, req.query, database, req.endpoint, httpMethod))
193223
}
194224
} catch (error) {
195225
if (error instanceof ValidationError) {
@@ -198,27 +228,31 @@ app.get('/collections/:collectionId/items', async (req, res, next) => {
198228
next(error)
199229
}
200230
}
201-
})
202-
203-
const DEFAULT_QUERYABLES = {
204-
$schema: 'https://json-schema.org/draft/2020-12/schema',
205-
type: 'object',
206-
properties: {},
207-
additionalProperties: true
208231
}
209232

210-
app.get('/collections/:collectionId/queryables', async (req, res, next) => {
233+
// are these supposed to be awaited?
234+
app.get('/collections/:collectionId/aggregate',
235+
async (req, res, next) => await collectionAggregate(req, res, next, 'GET'))
236+
237+
app.get('/collections/:collectionId/aggregate',
238+
async (req, res, next) => await collectionAggregate(req, res, next, 'POST'))
239+
240+
app.get('/collections/:collectionId/items', async (req, res, next) => {
211241
const { collectionId } = req.params
212242
try {
213-
const collection = await api.getCollection(collectionId, database, req.endpoint)
243+
const response = await api.getCollection(collectionId, database, req.endpoint)
214244

215-
if (collection instanceof Error) next(createError(404))
245+
if (response instanceof Error) next(createError(404))
216246
else {
217-
const queryables = collection.queryables || { ...DEFAULT_QUERYABLES }
218-
queryables.$id = `${req.endpoint}/collections/${collectionId}/queryables`
219-
queryables.title = `Queryables for Collection ${collectionId}`
220-
res.type('application/schema+json')
221-
res.json(queryables)
247+
const items = await api.searchItems(
248+
collectionId,
249+
req.query,
250+
database,
251+
req.endpoint,
252+
'GET'
253+
)
254+
res.type('application/geo+json')
255+
res.json(items)
222256
}
223257
} catch (error) {
224258
if (error instanceof ValidationError) {

0 commit comments

Comments
 (0)