Skip to content

Commit 9ed56e5

Browse files
committed
Merge branch 'collection_update_endpoint' of github.com:rhysrevans3/stac-fastapi-elasticsearch into collection_update_endpoint
2 parents 4e11011 + 6c6e9cb commit 9ed56e5

File tree

9 files changed

+324
-9
lines changed

9 files changed

+324
-9
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
- Advanced comparison (LIKE, IN, BETWEEN) operators to the Filter extension [#178](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/178)
13+
1214
### Changed
1315

1416
- Elasticsearch drivers from 7.17.9 to 8.11.0 [#169](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/169)
@@ -18,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1820

1921
- Exclude unset fields in search response [#166](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/166)
2022
- Upgrade stac-fastapi to v2.4.9 [#172](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/172)
23+
- Set correct default filter-lang for GET /search requests [#179](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/179)
2124

2225
## [v1.0.0]
2326

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
settings = ElasticsearchSettings()
2525
session = Session.create_from_settings(settings)
2626

27+
filter_extension = FilterExtension(client=EsAsyncBaseFiltersClient())
28+
filter_extension.conformance_classes.append(
29+
"http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators"
30+
)
31+
2732
extensions = [
2833
TransactionExtension(client=TransactionsClient(session=session), settings=settings),
2934
BulkTransactionExtension(client=BulkTransactionsClient(session=session)),
@@ -32,7 +37,7 @@
3237
SortExtension(),
3338
TokenPaginationExtension(),
3439
ContextExtension(),
35-
FilterExtension(client=EsAsyncBaseFiltersClient()),
40+
filter_extension,
3641
]
3742

3843
post_request_model = create_post_request_model(extensions)

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,12 @@ async def get_search(
380380
base_args["sortby"] = sort_param
381381

382382
if filter:
383-
if filter_lang == "cql2-text":
383+
if filter_lang == "cql2-json":
384384
base_args["filter-lang"] = "cql2-json"
385-
base_args["filter"] = orjson.loads(to_cql2(parse_cql2_text(filter)))
385+
base_args["filter"] = orjson.loads(unquote_plus(filter))
386386
else:
387387
base_args["filter-lang"] = "cql2-json"
388-
base_args["filter"] = orjson.loads(unquote_plus(filter))
388+
base_args["filter"] = orjson.loads(to_cql2(parse_cql2_text(filter)))
389389

390390
if fields:
391391
includes = set()

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/filter.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
Basic CQL2 (AND, OR, NOT), comparison operators (=, <>, <, <=, >, >=), and IS NULL.
55
The comparison operators are allowed against string, numeric, boolean, date, and datetime types.
66
7+
Advanced comparison operators (http://www.opengis.net/spec/cql2/1.0/req/advanced-comparison-operators)
8+
defines the LIKE, IN, and BETWEEN operators.
9+
710
Basic Spatial Operators (http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators)
811
defines the intersects operator (S_INTERSECTS).
912
"""
1013
from __future__ import annotations
1114

1215
import datetime
16+
import re
1317
from enum import Enum
1418
from typing import List, Union
1519

@@ -78,6 +82,17 @@ def to_es(self):
7882
)
7983

8084

85+
class AdvancedComparisonOp(str, Enum):
86+
"""Advanced Comparison operator.
87+
88+
CQL2 advanced comparison operators like (~), between, and in.
89+
"""
90+
91+
like = "like"
92+
between = "between"
93+
_in = "in"
94+
95+
8196
class SpatialIntersectsOp(str, Enum):
8297
"""Spatial intersections operator s_intersects."""
8398

@@ -152,8 +167,8 @@ def validate(cls, v):
152167
class Clause(BaseModel):
153168
"""Filter extension clause."""
154169

155-
op: Union[LogicalOp, ComparisonOp, SpatialIntersectsOp]
156-
args: List[Arg]
170+
op: Union[LogicalOp, ComparisonOp, AdvancedComparisonOp, SpatialIntersectsOp]
171+
args: List[Union[Arg, List[Arg]]]
157172

158173
def to_es(self):
159174
"""Generate an Elasticsearch expression for this Clause."""
@@ -171,6 +186,30 @@ def to_es(self):
171186
"must_not": [{"term": {to_es(self.args[0]): to_es(self.args[1])}}]
172187
}
173188
}
189+
elif self.op == AdvancedComparisonOp.like:
190+
return {
191+
"wildcard": {
192+
to_es(self.args[0]): {
193+
"value": cql2_like_to_es(str(to_es(self.args[1]))),
194+
"case_insensitive": "false",
195+
}
196+
}
197+
}
198+
elif self.op == AdvancedComparisonOp.between:
199+
return {
200+
"range": {
201+
to_es(self.args[0]): {
202+
"gte": to_es(self.args[1]),
203+
"lte": to_es(self.args[2]),
204+
}
205+
}
206+
}
207+
elif self.op == AdvancedComparisonOp._in:
208+
if not isinstance(self.args[1], List):
209+
raise RuntimeError(f"Arg {self.args[1]} is not a list")
210+
return {
211+
"terms": {to_es(self.args[0]): [to_es(arg) for arg in self.args[1]]}
212+
}
174213
elif (
175214
self.op == ComparisonOp.lt
176215
or self.op == ComparisonOp.lte
@@ -210,3 +249,19 @@ def to_es(arg: Arg):
210249
return arg
211250
else:
212251
raise RuntimeError(f"unknown arg {repr(arg)}")
252+
253+
254+
def cql2_like_to_es(string):
255+
"""Convert wildcard characters in CQL2 ('_' and '%') to Elasticsearch wildcard characters ('?' and '*', respectively). Handle escape characters and pass through Elasticsearch wildcards."""
256+
percent_pattern = r"(?<!\\)%"
257+
underscore_pattern = r"(?<!\\)_"
258+
escape_pattern = r"\\(?=[_%])"
259+
260+
for pattern in [
261+
(percent_pattern, "*"),
262+
(underscore_pattern, "?"),
263+
(escape_pattern, ""),
264+
]:
265+
string = re.sub(pattern[0], pattern[1], string)
266+
267+
return string
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"op": "like",
3+
"args": [
4+
{
5+
"property": "scene_id"
6+
},
7+
"LC82030282019133%"
8+
]
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"op": "like",
3+
"args": [
4+
{
5+
"property": "scene_id"
6+
},
7+
"LC82030282019133LGN0_"
8+
]
9+
}
10+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"op": "and",
3+
"args": [
4+
{
5+
"op": "between",
6+
"args": [
7+
{
8+
"property": "cloud_cover"
9+
},
10+
0.1,
11+
0.2
12+
]
13+
},
14+
{
15+
"op": "=",
16+
"args": [
17+
{
18+
"property": "landsat:wrs_row"
19+
},
20+
28
21+
]
22+
},
23+
{
24+
"op": "=",
25+
"args": [
26+
{
27+
"property": "landsat:wrs_path"
28+
},
29+
203
30+
]
31+
}
32+
]
33+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"op": "and",
3+
"args": [
4+
{
5+
"op": "in",
6+
"args": [
7+
{"property": "id"},
8+
["LC08_L1TP_060247_20180905_20180912_01_T1_L1TP"]
9+
]
10+
},
11+
{"op": "=", "args": [{"property": "collection"}, "landsat8_l1tp"]}
12+
]
13+
}

0 commit comments

Comments
 (0)