Skip to content

Commit 9aabbe0

Browse files
author
Phil Varner
authored
Implement Filter Extension (#100)
* Filter Extension implemenation
1 parent d6fdb5b commit 9aabbe0

File tree

22 files changed

+757
-24
lines changed

22 files changed

+757
-24
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
### Added
1313

14+
- Filter Extension as GET with CQL2-Text and POST with CQL2-JSON,
15+
supporting the Basic CQL2 and Basic Spatial Operators conformance classes.
16+
- Added Elasticsearch local config to support snapshot/restore to local filesystem
17+
1418
### Fixed
1519

16-
- Fixed search intersects query
20+
- Fixed search intersects query.
21+
- Corrected the Sort and Query conformance class URIs.
1722

1823
### Changed
1924

2025
- Default to Python 3.10
2126
- Default to Elasticsearch 8.x
2227
- Collection objects are now stored in `collections` index rather than `stac_collections` index
2328
- Item objects are no longer stored in `stac_items`, but in indices per collection named `items_{collection_id}`
29+
- When using bulk ingest, items will continue to be ingested if any of them fail. Previously, the call would fail
30+
immediately if any items failed.
2431

2532
### Removed
2633

stac_fastapi/elasticsearch/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"uvicorn",
2020
"overrides",
2121
"starlette",
22+
"geojson-pydantic",
23+
"pygeofilter==0.1.2",
2224
]
2325

2426
extra_reqs = {

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
from stac_fastapi.elasticsearch.core import (
1010
BulkTransactionsClient,
1111
CoreClient,
12+
EsAsyncBaseFiltersClient,
1213
TransactionsClient,
1314
)
1415
from stac_fastapi.elasticsearch.database_logic import create_collection_index
1516
from stac_fastapi.elasticsearch.extensions import QueryExtension
1617
from stac_fastapi.elasticsearch.session import Session
1718
from stac_fastapi.extensions.core import ( # FieldsExtension,
1819
ContextExtension,
20+
FilterExtension,
1921
SortExtension,
2022
TokenPaginationExtension,
2123
TransactionExtension,
@@ -26,17 +28,33 @@
2628
session = Session.create_from_settings(settings)
2729

2830

29-
# All of these extensions have their conformance class URL
30-
# incorrect, with an extra `/` before the #
3131
@attr.s
3232
class FixedSortExtension(SortExtension):
33-
"""Fixed Sort Extension string."""
33+
"""SortExtension class fixed with correct paths, removing extra forward-slash."""
3434

3535
conformance_classes: List[str] = attr.ib(
3636
factory=lambda: ["https://api.stacspec.org/v1.0.0-beta.4/item-search#sort"]
3737
)
3838

3939

40+
@attr.s
41+
class FixedFilterExtension(FilterExtension):
42+
"""FilterExtension class fixed with correct paths, removing extra forward-slash."""
43+
44+
conformance_classes: List[str] = attr.ib(
45+
default=[
46+
"https://api.stacspec.org/v1.0.0-rc.1/item-search#filter",
47+
"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter",
48+
"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter",
49+
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
50+
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-json",
51+
"http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
52+
"http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators",
53+
]
54+
)
55+
client = attr.ib(factory=EsAsyncBaseFiltersClient)
56+
57+
4058
@attr.s
4159
class FixedQueryExtension(QueryExtension):
4260
"""Fixed Query Extension string."""
@@ -54,6 +72,7 @@ class FixedQueryExtension(QueryExtension):
5472
FixedSortExtension(),
5573
TokenPaginationExtension(),
5674
ContextExtension(),
75+
FixedFilterExtension(),
5776
]
5877

5978
post_request_model = create_post_request_model(extensions)

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
from datetime import datetime as datetime_type
55
from datetime import timezone
6-
from typing import List, Optional, Type, Union
6+
from typing import Any, Dict, List, Optional, Type, Union
77
from urllib.parse import urljoin
88

99
import attr
@@ -21,12 +21,17 @@
2121
from stac_fastapi.elasticsearch.models.links import PagingLinks
2222
from stac_fastapi.elasticsearch.serializers import CollectionSerializer, ItemSerializer
2323
from stac_fastapi.elasticsearch.session import Session
24+
from stac_fastapi.extensions.core.filter.request import FilterLang
2425
from stac_fastapi.extensions.third_party.bulk_transactions import (
2526
BaseBulkTransactionsClient,
2627
Items,
2728
)
2829
from stac_fastapi.types import stac as stac_types
29-
from stac_fastapi.types.core import AsyncBaseCoreClient, AsyncBaseTransactionsClient
30+
from stac_fastapi.types.core import (
31+
AsyncBaseCoreClient,
32+
AsyncBaseFiltersClient,
33+
AsyncBaseTransactionsClient,
34+
)
3035
from stac_fastapi.types.links import CollectionLinks
3136
from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection
3237

@@ -172,6 +177,8 @@ async def get_search(
172177
token: Optional[str] = None,
173178
fields: Optional[List[str]] = None,
174179
sortby: Optional[str] = None,
180+
# filter: Optional[str] = None, # todo: requires fastapi > 2.3 unreleased
181+
# filter_lang: Optional[str] = None, # todo: requires fastapi > 2.3 unreleased
175182
**kwargs,
176183
) -> ItemCollection:
177184
"""GET search catalog."""
@@ -183,8 +190,10 @@ async def get_search(
183190
"token": token,
184191
"query": json.loads(query) if query else query,
185192
}
193+
186194
if datetime:
187195
base_args["datetime"] = datetime
196+
188197
if sortby:
189198
# https://github.com/radiantearth/stac-spec/tree/master/api-spec/extensions/sort#http-get-or-post-form
190199
sort_param = []
@@ -197,6 +206,13 @@ async def get_search(
197206
)
198207
base_args["sortby"] = sort_param
199208

209+
# todo: requires fastapi > 2.3 unreleased
210+
# if filter:
211+
# if filter_lang == "cql2-text":
212+
# base_args["filter-lang"] = "cql2-json"
213+
# base_args["filter"] = orjson.loads(to_cql2(parse_cql2_text(filter)))
214+
# print(f'>>> {base_args["filter"]}')
215+
200216
# if fields:
201217
# includes = set()
202218
# excludes = set()
@@ -264,6 +280,15 @@ async def post_search(
264280
search=search, op=op, field=field, value=value
265281
)
266282

283+
filter_lang = getattr(search_request, "filter_lang", None)
284+
285+
if hasattr(search_request, "filter"):
286+
cql2_filter = getattr(search_request, "filter", None)
287+
if filter_lang in [None, FilterLang.cql2_json]:
288+
search = self.database.apply_cql2_filter(search, cql2_filter)
289+
else:
290+
raise Exception("CQL2-Text is not supported with POST")
291+
267292
sort = None
268293
if search_request.sortby:
269294
sort = self.database.populate_sort(search_request.sortby)
@@ -455,3 +480,68 @@ def bulk_item_insert(
455480
)
456481

457482
return f"Successfully added {len(processed_items)} Items."
483+
484+
485+
@attr.s
486+
class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
487+
"""Defines a pattern for implementing the STAC filter extension."""
488+
489+
# todo: use the ES _mapping endpoint to dynamically find what fields exist
490+
async def get_queryables(
491+
self, collection_id: Optional[str] = None, **kwargs
492+
) -> Dict[str, Any]:
493+
"""Get the queryables available for the given collection_id.
494+
495+
If collection_id is None, returns the intersection of all
496+
queryables over all collections.
497+
498+
This base implementation returns a blank queryable schema. This is not allowed
499+
under OGC CQL but it is allowed by the STAC API Filter Extension
500+
501+
https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables
502+
"""
503+
return {
504+
"$schema": "https://json-schema.org/draft/2019-09/schema",
505+
"$id": "https://stac-api.example.com/queryables",
506+
"type": "object",
507+
"title": "Queryables for Example STAC API",
508+
"description": "Queryable names for the example STAC API Item Search filter.",
509+
"properties": {
510+
"id": {
511+
"description": "ID",
512+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
513+
},
514+
"collection": {
515+
"description": "Collection",
516+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
517+
},
518+
"geometry": {
519+
"description": "Geometry",
520+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
521+
},
522+
"datetime": {
523+
"description": "Acquisition Timestamp",
524+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
525+
},
526+
"created": {
527+
"description": "Creation Timestamp",
528+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/created",
529+
},
530+
"updated": {
531+
"description": "Creation Timestamp",
532+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
533+
},
534+
"cloud_cover": {
535+
"description": "Cloud Cover",
536+
"$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
537+
},
538+
"cloud_shadow_percentage": {
539+
"description": "Cloud Shadow Percentage",
540+
"title": "Cloud Shadow Percentage",
541+
"type": "number",
542+
"minimum": 0,
543+
"maximum": 100,
544+
},
545+
},
546+
"additionalProperties": True,
547+
}

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from stac_fastapi.elasticsearch.config import (
1414
ElasticsearchSettings as SyncElasticsearchSettings,
1515
)
16+
from stac_fastapi.elasticsearch.extensions import filter
1617
from stac_fastapi.types.errors import ConflictError, NotFoundError
1718
from stac_fastapi.types.stac import Collection, Item
1819

@@ -312,6 +313,13 @@ def apply_stacql_filter(search: Search, op: str, field: str, value: float):
312313

313314
return search
314315

316+
@staticmethod
317+
def apply_cql2_filter(search: Search, _filter: Optional[Dict[str, Any]]):
318+
"""Database logic to perform query for search endpoint."""
319+
if _filter is not None:
320+
search = search.filter(filter.Clause.parse_obj(_filter).to_es())
321+
return search
322+
315323
@staticmethod
316324
def populate_sort(sortby: List) -> Optional[Dict[str, Dict[str, str]]]:
317325
"""Database logic to sort search instance."""

0 commit comments

Comments
 (0)