Skip to content

Commit 125baff

Browse files
Fix IsNull operator (#330)
**Related Issue(s):** - #300 **Description:** - Upgrade `pygeofilter` to 0.3.1 - Resolves the issue with non-functional `IsNull` operator described in #300. - Includes a bugfix for datetime querying - Typo fix :) **PR Checklist:** - [x] Code is formatted and linted (run `pre-commit run --all-files`) - [x] Tests pass (run `make test`) - [x] Documentation has been updated to reflect changes, if applicable - [x] Changes are added to the changelog --------- Co-authored-by: Jonathan Healy <[email protected]>
1 parent fa312c8 commit 125baff

File tree

6 files changed

+76
-13
lines changed

6 files changed

+76
-13
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Changed
1111

1212
- Added note on the use of the default `*` use in route authentication dependecies. [#325](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/325)
13+
- Bugfixes for the `IsNull` operator and datetime filtering [#330](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/330)
1314

1415
## [v3.2.2] - 2024-12-15
1516

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ Authentication is an optional feature that can be enabled through `Route Depende
317317

318318
## Aggregation
319319

320-
Aggregation of points and geometries, as well as frequency distribution aggregation of any other property including dates is supported in stac-fatsapi-elasticsearch-opensearch. Aggregations can be defined at the root Catalog level (`/aggregations`) and at the Collection level (`/<collection_id>/aggregations`). Details for supported aggregations can be found at [./docs/src/aggregation.md](./docs/src/aggregation.md)
320+
Aggregation of points and geometries, as well as frequency distribution aggregation of any other property including dates is supported in stac-fatsapi-elasticsearch-opensearch. Aggregations can be defined at the root Catalog level (`/aggregations`) and at the Collection level (`/<collection_id>/aggregations`). Details for supported aggregations can be found in [the aggregation docs](./docs/src/aggregation.md)
321321

322322
## Rate Limiting
323323

docs/src/aggregation.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Aggregation
22

3-
Stac-fatsapi-elasticsearch-opensearch supports the STAC API [Aggregation Extension](https://github.com/stac-api-extensions/aggregation). This enables aggregation of points and geometries, as well as frequency distribution aggregation of any other property including dates. Aggregations can be defined at the root Catalog level (`/aggregations`) and at the Collection level (`/<collection_id>/aggregations`). The [Filter Extension](https://github.com/stac-api-extensions/filter) is also fully supported, enabling aggregated returns of search queries. Any query made with `/search` may also be executed with `/aggregate`, provided that the relevant aggregation fields are available,
3+
Stac-fastapi-elasticsearch-opensearch supports the STAC API [Aggregation Extension](https://github.com/stac-api-extensions/aggregation). This enables aggregation of points and geometries, as well as frequency distribution aggregation of any other property including dates. Aggregations can be defined at the root Catalog level (`/aggregations`) and at the Collection level (`/<collection_id>/aggregations`). The [Filter Extension](https://github.com/stac-api-extensions/filter) is also fully supported, enabling aggregated returns of search queries. Any query made with `/search` may also be executed with `/aggregate`, provided that the relevant aggregation fields are available,
44

55
A field named `aggregations` should be added to the Collection object for the collection for which the aggregations are available, for example:
66

stac_fastapi/core/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"orjson",
1717
"overrides",
1818
"geojson-pydantic",
19-
"pygeofilter==0.2.1",
19+
"pygeofilter==0.3.1",
2020
"typing_extensions==4.8.0",
2121
"jsonschema",
2222
"slowapi==0.1.9",

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

+22-10
Original file line numberDiff line numberDiff line change
@@ -140,26 +140,38 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
140140
ComparisonOp.GT,
141141
ComparisonOp.GTE,
142142
]:
143+
range_op = {
144+
ComparisonOp.LT: "lt",
145+
ComparisonOp.LTE: "lte",
146+
ComparisonOp.GT: "gt",
147+
ComparisonOp.GTE: "gte",
148+
}
149+
143150
field = to_es_field(query["args"][0]["property"])
144151
value = query["args"][1]
145152
if isinstance(value, dict) and "timestamp" in value:
146-
# Handle timestamp fields specifically
147153
value = value["timestamp"]
148-
if query["op"] == ComparisonOp.IS_NULL:
149-
return {"bool": {"must_not": {"exists": {"field": field}}}}
154+
if query["op"] == ComparisonOp.EQ:
155+
return {"range": {field: {"gte": value, "lte": value}}}
156+
elif query["op"] == ComparisonOp.NEQ:
157+
return {
158+
"bool": {
159+
"must_not": [{"range": {field: {"gte": value, "lte": value}}}]
160+
}
161+
}
162+
else:
163+
return {"range": {field: {range_op[query["op"]]: value}}}
150164
else:
151165
if query["op"] == ComparisonOp.EQ:
152166
return {"term": {field: value}}
153167
elif query["op"] == ComparisonOp.NEQ:
154168
return {"bool": {"must_not": [{"term": {field: value}}]}}
155169
else:
156-
range_op = {
157-
ComparisonOp.LT: "lt",
158-
ComparisonOp.LTE: "lte",
159-
ComparisonOp.GT: "gt",
160-
ComparisonOp.GTE: "gte",
161-
}[query["op"]]
162-
return {"range": {field: {range_op: value}}}
170+
return {"range": {field: {range_op[query["op"]]: value}}}
171+
172+
elif query["op"] == ComparisonOp.IS_NULL:
173+
field = to_es_field(query["args"][0]["property"])
174+
return {"bool": {"must_not": {"exists": {"field": field}}}}
163175

164176
elif query["op"] == AdvancedComparisonOp.BETWEEN:
165177
field = to_es_field(query["args"][0]["property"])

stac_fastapi/tests/extensions/test_filter.py

+50
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23
import os
34
from os import listdir
45
from os.path import isfile, join
@@ -48,6 +49,10 @@ async def test_search_filters_post(app_client, ctx):
4849

4950
for _filter in filters:
5051
resp = await app_client.post("/search", json={"filter": _filter})
52+
if resp.status_code != 200:
53+
logging.error(f"Failed with status {resp.status_code}")
54+
logging.error(f"Response body: {resp.json()}")
55+
logging.error({"filter": _filter})
5156
assert resp.status_code == 200
5257

5358

@@ -431,3 +436,48 @@ async def test_search_filter_extension_between(app_client, ctx):
431436

432437
assert resp.status_code == 200
433438
assert len(resp.json()["features"]) == 1
439+
440+
441+
@pytest.mark.asyncio
442+
async def test_search_filter_extension_isnull_post(app_client, ctx):
443+
# Test for a property that is not null
444+
params = {
445+
"filter-lang": "cql2-json",
446+
"filter": {
447+
"op": "isNull",
448+
"args": [{"property": "properties.view:sun_elevation"}],
449+
},
450+
}
451+
resp = await app_client.post("/search", json=params)
452+
453+
assert resp.status_code == 200
454+
assert len(resp.json()["features"]) == 0
455+
456+
# Test for the property that is null
457+
params = {
458+
"filter-lang": "cql2-json",
459+
"filter": {
460+
"op": "isNull",
461+
"args": [{"property": "properties.thispropertyisnull"}],
462+
},
463+
}
464+
resp = await app_client.post("/search", json=params)
465+
466+
assert resp.status_code == 200
467+
assert len(resp.json()["features"]) == 1
468+
469+
470+
@pytest.mark.asyncio
471+
async def test_search_filter_extension_isnull_get(app_client, ctx):
472+
# Test for a property that is not null
473+
474+
resp = await app_client.get("/search?filter=properties.view:sun_elevation IS NULL")
475+
476+
assert resp.status_code == 200
477+
assert len(resp.json()["features"]) == 0
478+
479+
# Test for the property that is null
480+
resp = await app_client.get("/search?filter=properties.thispropertyisnull IS NULL")
481+
482+
assert resp.status_code == 200
483+
assert len(resp.json()["features"]) == 1

0 commit comments

Comments
 (0)