Skip to content

Commit 8ad2cd6

Browse files
committed
Merge branch 'main' into sfeos-helpers
2 parents d5b5009 + eec5f14 commit 8ad2cd6

File tree

19 files changed

+212
-44
lines changed

19 files changed

+212
-44
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
### Changed
13+
14+
### Fixed
15+
16+
17+
## [v4.2.0] - 2025-05-15
18+
19+
### Added
20+
21+
- Added dynamic queryables mapping for search and aggregations [#375](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/375)
1222
- Added configurable landing page ID `STAC_FASTAPI_LANDING_PAGE_ID` [#352](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/352)
1323
- Added support for `S_CONTAINS`, `S_WITHIN`, `S_DISJOINT` spatial filter operations [#371](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/371)
1424
- Introduced the `DATABASE_REFRESH` environment variable to control whether database operations refresh the index immediately after changes. If set to `true`, changes will be immediately searchable. If set to `false`, changes may not be immediately visible but can improve performance for bulk operations. If set to `wait_for`, changes will wait for the next refresh cycle to become visible. [#370](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/370)
@@ -378,7 +388,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
378388
- Use genexp in execute_search and get_all_collections to return results.
379389
- Added db_to_stac serializer to item_collection method in core.py.
380390

381-
[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.1.0...main
391+
[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.2.0...main
392+
[v4.2.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.1.0...v4.2.0
382393
[v4.1.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.0.0...v4.1.0
383394
[v4.0.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v3.2.5...v4.0.0
384395
[v3.2.5]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v3.2.4...v3.2.5

compose.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.1.0
12+
- STAC_FASTAPI_VERSION=4.2.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -42,7 +42,8 @@ services:
4242
environment:
4343
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4444
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
45-
- STAC_FASTAPI_VERSION=4.1.0
45+
- STAC_FASTAPI_VERSION=4.2.0
46+
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-opensearch
4647
- APP_HOST=0.0.0.0
4748
- APP_PORT=8082
4849
- RELOAD=true

examples/auth/compose.basic_auth.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.1.0
12+
- STAC_FASTAPI_VERSION=4.2.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -43,7 +43,8 @@ services:
4343
environment:
4444
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4545
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
46-
- STAC_FASTAPI_VERSION=4.1.0
46+
- STAC_FASTAPI_VERSION=4.2.0
47+
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-opensearch
4748
- APP_HOST=0.0.0.0
4849
- APP_PORT=8082
4950
- RELOAD=true

examples/auth/compose.oauth2.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.1.0
12+
- STAC_FASTAPI_VERSION=4.2.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -44,7 +44,8 @@ services:
4444
environment:
4545
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4646
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
47-
- STAC_FASTAPI_VERSION=4.1.0
47+
- STAC_FASTAPI_VERSION=4.2.0
48+
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-opensearch
4849
- APP_HOST=0.0.0.0
4950
- APP_PORT=8082
5051
- RELOAD=true

examples/auth/compose.route_dependencies.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.1.0
12+
- STAC_FASTAPI_VERSION=4.2.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -43,7 +43,8 @@ services:
4343
environment:
4444
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4545
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
46-
- STAC_FASTAPI_VERSION=4.1.0
46+
- STAC_FASTAPI_VERSION=4.2.0
47+
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-opensearch
4748
- APP_HOST=0.0.0.0
4849
- APP_PORT=8082
4950
- RELOAD=true

examples/rate_limit/compose.rate_limit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.1.0
12+
- STAC_FASTAPI_VERSION=4.2.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -43,7 +43,7 @@ services:
4343
environment:
4444
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4545
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
46-
- STAC_FASTAPI_VERSION=4.1.0
46+
- STAC_FASTAPI_VERSION=4.2.0
4747
- APP_HOST=0.0.0.0
4848
- APP_PORT=8082
4949
- RELOAD=true

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ async def post_search(
524524
if hasattr(search_request, "filter_expr"):
525525
cql2_filter = getattr(search_request, "filter_expr", None)
526526
try:
527-
search = self.database.apply_cql2_filter(search, cql2_filter)
527+
search = await self.database.apply_cql2_filter(search, cql2_filter)
528528
except Exception as e:
529529
raise HTTPException(
530530
status_code=400, detail=f"Error with cql2_json filter: {e}"

stac_fastapi/core/stac_fastapi/core/extensions/aggregation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ async def aggregate(
467467

468468
if aggregate_request.filter_expr:
469469
try:
470-
search = self.database.apply_cql2_filter(
470+
search = await self.database.apply_cql2_filter(
471471
search, aggregate_request.filter_expr
472472
)
473473
except Exception as e:

stac_fastapi/core/stac_fastapi/core/extensions/filter.py

Lines changed: 109 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,7 @@ class SpatialOp(str, Enum):
136136
S_DISJOINT = "s_disjoint"
137137

138138

139-
queryables_mapping = {
140-
"id": "id",
141-
"collection": "collection",
142-
"geometry": "geometry",
143-
"datetime": "properties.datetime",
144-
"created": "properties.created",
145-
"updated": "properties.updated",
146-
"cloud_cover": "properties.eo:cloud_cover",
147-
"cloud_shadow_percentage": "properties.s2:cloud_shadow_percentage",
148-
"nodata_pixel_percentage": "properties.s2:nodata_pixel_percentage",
149-
}
150-
151-
152-
def to_es_field(field: str) -> str:
139+
def to_es_field(queryables_mapping: Dict[str, Any], field: str) -> str:
153140
"""
154141
Map a given field to its corresponding Elasticsearch field according to a predefined mapping.
155142
@@ -160,3 +147,111 @@ def to_es_field(field: str) -> str:
160147
str: The mapped field name suitable for Elasticsearch queries.
161148
"""
162149
return queryables_mapping.get(field, field)
150+
151+
152+
def to_es(queryables_mapping: Dict[str, Any], query: Dict[str, Any]) -> Dict[str, Any]:
153+
"""
154+
Transform a simplified CQL2 query structure to an Elasticsearch compatible query DSL.
155+
156+
Args:
157+
query (Dict[str, Any]): The query dictionary containing 'op' and 'args'.
158+
159+
Returns:
160+
Dict[str, Any]: The corresponding Elasticsearch query in the form of a dictionary.
161+
"""
162+
if query["op"] in [LogicalOp.AND, LogicalOp.OR, LogicalOp.NOT]:
163+
bool_type = {
164+
LogicalOp.AND: "must",
165+
LogicalOp.OR: "should",
166+
LogicalOp.NOT: "must_not",
167+
}[query["op"]]
168+
return {
169+
"bool": {
170+
bool_type: [
171+
to_es(queryables_mapping, sub_query) for sub_query in query["args"]
172+
]
173+
}
174+
}
175+
176+
elif query["op"] in [
177+
ComparisonOp.EQ,
178+
ComparisonOp.NEQ,
179+
ComparisonOp.LT,
180+
ComparisonOp.LTE,
181+
ComparisonOp.GT,
182+
ComparisonOp.GTE,
183+
]:
184+
range_op = {
185+
ComparisonOp.LT: "lt",
186+
ComparisonOp.LTE: "lte",
187+
ComparisonOp.GT: "gt",
188+
ComparisonOp.GTE: "gte",
189+
}
190+
191+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
192+
value = query["args"][1]
193+
if isinstance(value, dict) and "timestamp" in value:
194+
value = value["timestamp"]
195+
if query["op"] == ComparisonOp.EQ:
196+
return {"range": {field: {"gte": value, "lte": value}}}
197+
elif query["op"] == ComparisonOp.NEQ:
198+
return {
199+
"bool": {
200+
"must_not": [{"range": {field: {"gte": value, "lte": value}}}]
201+
}
202+
}
203+
else:
204+
return {"range": {field: {range_op[query["op"]]: value}}}
205+
else:
206+
if query["op"] == ComparisonOp.EQ:
207+
return {"term": {field: value}}
208+
elif query["op"] == ComparisonOp.NEQ:
209+
return {"bool": {"must_not": [{"term": {field: value}}]}}
210+
else:
211+
return {"range": {field: {range_op[query["op"]]: value}}}
212+
213+
elif query["op"] == ComparisonOp.IS_NULL:
214+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
215+
return {"bool": {"must_not": {"exists": {"field": field}}}}
216+
217+
elif query["op"] == AdvancedComparisonOp.BETWEEN:
218+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
219+
gte, lte = query["args"][1], query["args"][2]
220+
if isinstance(gte, dict) and "timestamp" in gte:
221+
gte = gte["timestamp"]
222+
if isinstance(lte, dict) and "timestamp" in lte:
223+
lte = lte["timestamp"]
224+
return {"range": {field: {"gte": gte, "lte": lte}}}
225+
226+
elif query["op"] == AdvancedComparisonOp.IN:
227+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
228+
values = query["args"][1]
229+
if not isinstance(values, list):
230+
raise ValueError(f"Arg {values} is not a list")
231+
return {"terms": {field: values}}
232+
233+
elif query["op"] == AdvancedComparisonOp.LIKE:
234+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
235+
pattern = cql2_like_to_es(query["args"][1])
236+
return {"wildcard": {field: {"value": pattern, "case_insensitive": True}}}
237+
238+
elif query["op"] in [
239+
SpatialOp.S_INTERSECTS,
240+
SpatialOp.S_CONTAINS,
241+
SpatialOp.S_WITHIN,
242+
SpatialOp.S_DISJOINT,
243+
]:
244+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
245+
geometry = query["args"][1]
246+
247+
relation_mapping = {
248+
SpatialOp.S_INTERSECTS: "intersects",
249+
SpatialOp.S_CONTAINS: "contains",
250+
SpatialOp.S_WITHIN: "within",
251+
SpatialOp.S_DISJOINT: "disjoint",
252+
}
253+
254+
relation = relation_mapping[query["op"]]
255+
return {"geo_shape": {field: {"shape": geometry, "relation": relation}}}
256+
257+
return {}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""library version."""
2-
__version__ = "4.1.0"
2+
__version__ = "4.2.0"

0 commit comments

Comments
 (0)