stac-fastapi-opensearch 6.4.0__py3-none-any.whl → 6.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_fastapi/opensearch/app.py +74 -9
- stac_fastapi/opensearch/database_logic.py +173 -19
- stac_fastapi/opensearch/version.py +1 -1
- {stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/METADATA +34 -12
- stac_fastapi_opensearch-6.5.1.dist-info/RECORD +10 -0
- stac_fastapi_opensearch-6.4.0.dist-info/RECORD +0 -10
- {stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/WHEEL +0 -0
- {stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/entry_points.txt +0 -0
- {stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/top_level.txt +0 -0
stac_fastapi/opensearch/app.py
CHANGED
|
@@ -23,14 +23,19 @@ from stac_fastapi.core.extensions.aggregation import (
|
|
|
23
23
|
EsAggregationExtensionGetRequest,
|
|
24
24
|
EsAggregationExtensionPostRequest,
|
|
25
25
|
)
|
|
26
|
+
from stac_fastapi.core.extensions.collections_search import (
|
|
27
|
+
CollectionsSearchEndpointExtension,
|
|
28
|
+
)
|
|
26
29
|
from stac_fastapi.core.extensions.fields import FieldsExtension
|
|
27
30
|
from stac_fastapi.core.rate_limit import setup_rate_limit
|
|
28
31
|
from stac_fastapi.core.route_dependencies import get_route_dependencies
|
|
29
32
|
from stac_fastapi.core.session import Session
|
|
30
33
|
from stac_fastapi.core.utilities import get_bool_env
|
|
31
|
-
from stac_fastapi.extensions.core import (
|
|
34
|
+
from stac_fastapi.extensions.core import (
|
|
32
35
|
AggregationExtension,
|
|
33
36
|
CollectionSearchExtension,
|
|
37
|
+
CollectionSearchFilterExtension,
|
|
38
|
+
CollectionSearchPostExtension,
|
|
34
39
|
FilterExtension,
|
|
35
40
|
FreeTextExtension,
|
|
36
41
|
SortExtension,
|
|
@@ -57,8 +62,14 @@ logger = logging.getLogger(__name__)
|
|
|
57
62
|
|
|
58
63
|
TRANSACTIONS_EXTENSIONS = get_bool_env("ENABLE_TRANSACTIONS_EXTENSIONS", default=True)
|
|
59
64
|
ENABLE_COLLECTIONS_SEARCH = get_bool_env("ENABLE_COLLECTIONS_SEARCH", default=True)
|
|
65
|
+
ENABLE_COLLECTIONS_SEARCH_ROUTE = get_bool_env(
|
|
66
|
+
"ENABLE_COLLECTIONS_SEARCH_ROUTE", default=False
|
|
67
|
+
)
|
|
60
68
|
logger.info("TRANSACTIONS_EXTENSIONS is set to %s", TRANSACTIONS_EXTENSIONS)
|
|
61
69
|
logger.info("ENABLE_COLLECTIONS_SEARCH is set to %s", ENABLE_COLLECTIONS_SEARCH)
|
|
70
|
+
logger.info(
|
|
71
|
+
"ENABLE_COLLECTIONS_SEARCH_ROUTE is set to %s", ENABLE_COLLECTIONS_SEARCH_ROUTE
|
|
72
|
+
)
|
|
62
73
|
|
|
63
74
|
settings = OpensearchSettings()
|
|
64
75
|
session = Session.create_from_settings(settings)
|
|
@@ -116,16 +127,18 @@ if TRANSACTIONS_EXTENSIONS:
|
|
|
116
127
|
|
|
117
128
|
extensions = [aggregation_extension] + search_extensions
|
|
118
129
|
|
|
119
|
-
#
|
|
120
|
-
|
|
130
|
+
# Collection search related variables
|
|
131
|
+
collections_get_request_model = None
|
|
132
|
+
|
|
133
|
+
if ENABLE_COLLECTIONS_SEARCH or ENABLE_COLLECTIONS_SEARCH_ROUTE:
|
|
121
134
|
# Create collection search extensions
|
|
122
135
|
collection_search_extensions = [
|
|
123
|
-
|
|
136
|
+
QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
|
|
124
137
|
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
|
|
125
138
|
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
CollectionSearchFilterExtension(
|
|
140
|
+
conformance_classes=[FilterConformanceClasses.COLLECTIONS]
|
|
141
|
+
),
|
|
129
142
|
FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]),
|
|
130
143
|
]
|
|
131
144
|
|
|
@@ -135,7 +148,58 @@ if ENABLE_COLLECTIONS_SEARCH:
|
|
|
135
148
|
)
|
|
136
149
|
collections_get_request_model = collection_search_ext.GET
|
|
137
150
|
|
|
151
|
+
# Create a post request model for collection search
|
|
152
|
+
collection_search_post_request_model = create_post_request_model(
|
|
153
|
+
collection_search_extensions
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Create collection search extensions if enabled
|
|
157
|
+
if ENABLE_COLLECTIONS_SEARCH:
|
|
158
|
+
# Initialize collection search POST extension
|
|
159
|
+
collection_search_post_ext = CollectionSearchPostExtension(
|
|
160
|
+
client=CoreClient(
|
|
161
|
+
database=database_logic,
|
|
162
|
+
session=session,
|
|
163
|
+
post_request_model=collection_search_post_request_model,
|
|
164
|
+
landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
|
|
165
|
+
),
|
|
166
|
+
settings=settings,
|
|
167
|
+
POST=collection_search_post_request_model,
|
|
168
|
+
conformance_classes=[
|
|
169
|
+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search",
|
|
170
|
+
QueryConformanceClasses.COLLECTIONS,
|
|
171
|
+
FilterConformanceClasses.COLLECTIONS,
|
|
172
|
+
FreeTextConformanceClasses.COLLECTIONS,
|
|
173
|
+
SortConformanceClasses.COLLECTIONS,
|
|
174
|
+
FieldsConformanceClasses.COLLECTIONS,
|
|
175
|
+
],
|
|
176
|
+
)
|
|
138
177
|
extensions.append(collection_search_ext)
|
|
178
|
+
extensions.append(collection_search_post_ext)
|
|
179
|
+
|
|
180
|
+
if ENABLE_COLLECTIONS_SEARCH_ROUTE:
|
|
181
|
+
# Initialize collections-search endpoint extension
|
|
182
|
+
collections_search_endpoint_ext = CollectionsSearchEndpointExtension(
|
|
183
|
+
client=CoreClient(
|
|
184
|
+
database=database_logic,
|
|
185
|
+
session=session,
|
|
186
|
+
post_request_model=collection_search_post_request_model,
|
|
187
|
+
landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
|
|
188
|
+
),
|
|
189
|
+
settings=settings,
|
|
190
|
+
GET=collections_get_request_model,
|
|
191
|
+
POST=collection_search_post_request_model,
|
|
192
|
+
conformance_classes=[
|
|
193
|
+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search",
|
|
194
|
+
QueryConformanceClasses.COLLECTIONS,
|
|
195
|
+
FilterConformanceClasses.COLLECTIONS,
|
|
196
|
+
FreeTextConformanceClasses.COLLECTIONS,
|
|
197
|
+
SortConformanceClasses.COLLECTIONS,
|
|
198
|
+
FieldsConformanceClasses.COLLECTIONS,
|
|
199
|
+
],
|
|
200
|
+
)
|
|
201
|
+
extensions.append(collections_search_endpoint_ext)
|
|
202
|
+
|
|
139
203
|
|
|
140
204
|
database_logic.extensions = [type(ext).__name__ for ext in extensions]
|
|
141
205
|
|
|
@@ -169,14 +233,15 @@ app_config = {
|
|
|
169
233
|
post_request_model=post_request_model,
|
|
170
234
|
landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
|
|
171
235
|
),
|
|
236
|
+
"collections_get_request_model": collections_get_request_model,
|
|
172
237
|
"search_get_request_model": create_get_request_model(search_extensions),
|
|
173
238
|
"search_post_request_model": post_request_model,
|
|
174
239
|
"items_get_request_model": items_get_request_model,
|
|
175
240
|
"route_dependencies": get_route_dependencies(),
|
|
176
241
|
}
|
|
177
242
|
|
|
178
|
-
# Add collections_get_request_model if
|
|
179
|
-
if
|
|
243
|
+
# Add collections_get_request_model if it was created
|
|
244
|
+
if collections_get_request_model:
|
|
180
245
|
app_config["collections_get_request_model"] = collections_get_request_model
|
|
181
246
|
|
|
182
247
|
api = StacApi(**app_config)
|
|
@@ -160,8 +160,11 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
160
160
|
request: Request,
|
|
161
161
|
sort: Optional[List[Dict[str, Any]]] = None,
|
|
162
162
|
q: Optional[List[str]] = None,
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
164
|
+
query: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
165
|
+
datetime: Optional[str] = None,
|
|
166
|
+
) -> Tuple[List[Dict[str, Any]], Optional[str], Optional[int]]:
|
|
167
|
+
"""Retrieve a list of collections from OpenSearch, supporting pagination.
|
|
165
168
|
|
|
166
169
|
Args:
|
|
167
170
|
token (Optional[str]): The pagination token.
|
|
@@ -169,6 +172,9 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
169
172
|
request (Request): The FastAPI request object.
|
|
170
173
|
sort (Optional[List[Dict[str, Any]]]): Optional sort parameter from the request.
|
|
171
174
|
q (Optional[List[str]]): Free text search terms.
|
|
175
|
+
query (Optional[Dict[str, Dict[str, Any]]]): Query extension parameters.
|
|
176
|
+
filter (Optional[Dict[str, Any]]): Structured query in CQL2 format.
|
|
177
|
+
datetime (Optional[str]): Temporal filter.
|
|
172
178
|
|
|
173
179
|
Returns:
|
|
174
180
|
A tuple of (collections, next pagination token if any).
|
|
@@ -206,8 +212,24 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
206
212
|
"size": limit,
|
|
207
213
|
}
|
|
208
214
|
|
|
215
|
+
# Handle search_after token - split by '|' to get all sort values
|
|
216
|
+
search_after = None
|
|
209
217
|
if token:
|
|
210
|
-
|
|
218
|
+
try:
|
|
219
|
+
# The token should be a pipe-separated string of sort values
|
|
220
|
+
# e.g., "2023-01-01T00:00:00Z|collection-1"
|
|
221
|
+
search_after = token.split("|")
|
|
222
|
+
# If the number of sort fields doesn't match token parts, ignore the token
|
|
223
|
+
if len(search_after) != len(formatted_sort):
|
|
224
|
+
search_after = None
|
|
225
|
+
except Exception:
|
|
226
|
+
search_after = None
|
|
227
|
+
|
|
228
|
+
if search_after is not None:
|
|
229
|
+
body["search_after"] = search_after
|
|
230
|
+
|
|
231
|
+
# Build the query part of the body
|
|
232
|
+
query_parts = []
|
|
211
233
|
|
|
212
234
|
# Apply free text query if provided
|
|
213
235
|
if q:
|
|
@@ -235,16 +257,86 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
235
257
|
}
|
|
236
258
|
)
|
|
237
259
|
|
|
238
|
-
# Add the query to the
|
|
239
|
-
|
|
240
|
-
"bool": {"should": should_clauses, "minimum_should_match": 1}
|
|
241
|
-
|
|
260
|
+
# Add the free text query to the query parts
|
|
261
|
+
query_parts.append(
|
|
262
|
+
{"bool": {"should": should_clauses, "minimum_should_match": 1}}
|
|
263
|
+
)
|
|
242
264
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
265
|
+
# Apply structured filter if provided
|
|
266
|
+
if filter:
|
|
267
|
+
# Convert string filter to dict if needed
|
|
268
|
+
if isinstance(filter, str):
|
|
269
|
+
filter = orjson.loads(filter)
|
|
270
|
+
# Convert the filter to an OpenSearch query using the filter module
|
|
271
|
+
es_query = filter_module.to_es(await self.get_queryables_mapping(), filter)
|
|
272
|
+
query_parts.append(es_query)
|
|
273
|
+
|
|
274
|
+
# Apply query extension if provided
|
|
275
|
+
if query:
|
|
276
|
+
try:
|
|
277
|
+
# First create a search object to apply filters
|
|
278
|
+
search = Search(index=COLLECTIONS_INDEX)
|
|
279
|
+
|
|
280
|
+
# Process each field and operator in the query
|
|
281
|
+
for field_name, expr in query.items():
|
|
282
|
+
for op, value in expr.items():
|
|
283
|
+
# For collections, we don't need to prefix with 'properties__'
|
|
284
|
+
field = field_name
|
|
285
|
+
# Apply the filter using apply_stacql_filter
|
|
286
|
+
search = self.apply_stacql_filter(
|
|
287
|
+
search=search, op=op, field=field, value=value
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Convert the search object to a query dict and add it to query_parts
|
|
291
|
+
search_dict = search.to_dict()
|
|
292
|
+
if "query" in search_dict:
|
|
293
|
+
query_parts.append(search_dict["query"])
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Error converting query to OpenSearch: {e}")
|
|
297
|
+
# If there's an error, add a query that matches nothing
|
|
298
|
+
query_parts.append({"bool": {"must_not": {"match_all": {}}}})
|
|
299
|
+
raise
|
|
300
|
+
|
|
301
|
+
# Combine all query parts with AND logic if there are multiple
|
|
302
|
+
datetime_filter = None
|
|
303
|
+
if datetime:
|
|
304
|
+
datetime_filter = self._apply_collection_datetime_filter(datetime)
|
|
305
|
+
if datetime_filter:
|
|
306
|
+
query_parts.append(datetime_filter)
|
|
307
|
+
|
|
308
|
+
# Combine all query parts with AND logic
|
|
309
|
+
if query_parts:
|
|
310
|
+
body["query"] = (
|
|
311
|
+
query_parts[0]
|
|
312
|
+
if len(query_parts) == 1
|
|
313
|
+
else {"bool": {"must": query_parts}}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Create a copy of the body for count query (without pagination and sorting)
|
|
317
|
+
count_body = body.copy()
|
|
318
|
+
if "search_after" in count_body:
|
|
319
|
+
del count_body["search_after"]
|
|
320
|
+
count_body["size"] = 0
|
|
321
|
+
|
|
322
|
+
# Create async tasks for both search and count
|
|
323
|
+
search_task = asyncio.create_task(
|
|
324
|
+
self.client.search(
|
|
325
|
+
index=COLLECTIONS_INDEX,
|
|
326
|
+
body=body,
|
|
327
|
+
)
|
|
246
328
|
)
|
|
247
329
|
|
|
330
|
+
count_task = asyncio.create_task(
|
|
331
|
+
self.client.count(
|
|
332
|
+
index=COLLECTIONS_INDEX,
|
|
333
|
+
body={"query": body.get("query", {"match_all": {}})},
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Wait for search task to complete
|
|
338
|
+
response = await search_task
|
|
339
|
+
|
|
248
340
|
hits = response["hits"]["hits"]
|
|
249
341
|
collections = [
|
|
250
342
|
self.collection_serializer.db_to_stac(
|
|
@@ -255,12 +347,26 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
255
347
|
|
|
256
348
|
next_token = None
|
|
257
349
|
if len(hits) == limit:
|
|
258
|
-
# Ensure we have a valid sort value for next_token
|
|
259
350
|
next_token_values = hits[-1].get("sort")
|
|
260
351
|
if next_token_values:
|
|
261
|
-
|
|
352
|
+
# Join all sort values with '|' to create the token
|
|
353
|
+
next_token = "|".join(str(val) for val in next_token_values)
|
|
262
354
|
|
|
263
|
-
|
|
355
|
+
# Get the total count of collections
|
|
356
|
+
matched = (
|
|
357
|
+
response["hits"]["total"]["value"]
|
|
358
|
+
if response["hits"]["total"]["relation"] == "eq"
|
|
359
|
+
else None
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# If count task is done, use its result
|
|
363
|
+
if count_task.done():
|
|
364
|
+
try:
|
|
365
|
+
matched = count_task.result().get("count")
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.error(f"Count task failed: {e}")
|
|
368
|
+
|
|
369
|
+
return collections, next_token, matched
|
|
264
370
|
|
|
265
371
|
async def get_one_item(self, collection_id: str, item_id: str) -> Dict:
|
|
266
372
|
"""Retrieve a single item from the database.
|
|
@@ -276,7 +382,7 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
276
382
|
NotFoundError: If the specified Item does not exist in the Collection.
|
|
277
383
|
|
|
278
384
|
Notes:
|
|
279
|
-
The Item is retrieved from the
|
|
385
|
+
The Item is retrieved from the Opensearch database using the `client.get` method,
|
|
280
386
|
with the index for the Collection as the target index and the combined `mk_item_id` as the document id.
|
|
281
387
|
"""
|
|
282
388
|
try:
|
|
@@ -348,6 +454,41 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
348
454
|
search=search, free_text_queries=free_text_queries
|
|
349
455
|
)
|
|
350
456
|
|
|
457
|
+
@staticmethod
|
|
458
|
+
def _apply_collection_datetime_filter(
|
|
459
|
+
datetime_str: Optional[str],
|
|
460
|
+
) -> Optional[Dict[str, Any]]:
|
|
461
|
+
"""Create a temporal filter for collections based on their extent."""
|
|
462
|
+
if not datetime_str:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
# Parse the datetime string into start and end
|
|
466
|
+
if "/" in datetime_str:
|
|
467
|
+
start, end = datetime_str.split("/")
|
|
468
|
+
# Replace open-ended ranges with concrete dates
|
|
469
|
+
if start == "..":
|
|
470
|
+
# For open-ended start, use a very early date
|
|
471
|
+
start = "1800-01-01T00:00:00Z"
|
|
472
|
+
if end == "..":
|
|
473
|
+
# For open-ended end, use a far future date
|
|
474
|
+
end = "2999-12-31T23:59:59Z"
|
|
475
|
+
else:
|
|
476
|
+
# If it's just a single date, use it for both start and end
|
|
477
|
+
start = end = datetime_str
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
"bool": {
|
|
481
|
+
"must": [
|
|
482
|
+
# Check if any date in the array is less than or equal to the query end date
|
|
483
|
+
# This will match if the collection's start date is before or equal to the query end date
|
|
484
|
+
{"range": {"extent.temporal.interval": {"lte": end}}},
|
|
485
|
+
# Check if any date in the array is greater than or equal to the query start date
|
|
486
|
+
# This will match if the collection's end date is after or equal to the query start date
|
|
487
|
+
{"range": {"extent.temporal.interval": {"gte": start}}},
|
|
488
|
+
]
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
351
492
|
@staticmethod
|
|
352
493
|
def apply_datetime_filter(
|
|
353
494
|
search: Search, datetime: Optional[str]
|
|
@@ -542,18 +683,31 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
542
683
|
|
|
543
684
|
Args:
|
|
544
685
|
search (Search): The search object to apply the filter to.
|
|
545
|
-
op (str): The comparison operator to use. Can be 'eq' (equal), '
|
|
546
|
-
'lt' (less than), or 'lte' (less than or equal).
|
|
686
|
+
op (str): The comparison operator to use. Can be 'eq' (equal), 'ne'/'neq' (not equal), 'gt' (greater than),
|
|
687
|
+
'gte' (greater than or equal), 'lt' (less than), or 'lte' (less than or equal).
|
|
547
688
|
field (str): The field to perform the comparison on.
|
|
548
689
|
value (float): The value to compare the field against.
|
|
549
690
|
|
|
550
691
|
Returns:
|
|
551
692
|
search (Search): The search object with the specified filter applied.
|
|
552
693
|
"""
|
|
553
|
-
if op
|
|
554
|
-
|
|
694
|
+
if op == "eq":
|
|
695
|
+
search = search.filter("term", **{field: value})
|
|
696
|
+
elif op == "ne" or op == "neq":
|
|
697
|
+
# For not equal, use a bool query with must_not
|
|
698
|
+
search = search.exclude("term", **{field: value})
|
|
699
|
+
elif op in ["gt", "gte", "lt", "lte"]:
|
|
700
|
+
# For range operators
|
|
701
|
+
key_filter = {field: {op: value}}
|
|
555
702
|
search = search.filter(Q("range", **key_filter))
|
|
556
|
-
|
|
703
|
+
elif op == "in":
|
|
704
|
+
# For in operator (value should be a list)
|
|
705
|
+
if isinstance(value, list):
|
|
706
|
+
search = search.filter("terms", **{field: value})
|
|
707
|
+
else:
|
|
708
|
+
search = search.filter("term", **{field: value})
|
|
709
|
+
elif op == "contains":
|
|
710
|
+
# For contains operator (for arrays)
|
|
557
711
|
search = search.filter("term", **{field: value})
|
|
558
712
|
|
|
559
713
|
return search
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""library version."""
|
|
2
|
-
__version__ = "6.
|
|
2
|
+
__version__ = "6.5.1"
|
{stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi-opensearch
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.5.1
|
|
4
4
|
Summary: Opensearch stac-fastapi backend.
|
|
5
5
|
Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
|
|
6
6
|
License: MIT
|
|
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: stac-fastapi-core==6.
|
|
19
|
-
Requires-Dist: sfeos-helpers==6.
|
|
18
|
+
Requires-Dist: stac-fastapi-core==6.5.1
|
|
19
|
+
Requires-Dist: sfeos-helpers==6.5.1
|
|
20
20
|
Requires-Dist: opensearch-py~=2.8.0
|
|
21
21
|
Requires-Dist: opensearch-py[async]~=2.8.0
|
|
22
22
|
Requires-Dist: uvicorn~=0.23.0
|
|
@@ -103,13 +103,13 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
|
|
|
103
103
|
## Table of Contents
|
|
104
104
|
|
|
105
105
|
- [stac-fastapi-elasticsearch-opensearch](#stac-fastapi-elasticsearch-opensearch)
|
|
106
|
-
- [Sponsors
|
|
106
|
+
- [Sponsors & Supporters](#sponsors--supporters)
|
|
107
107
|
- [Project Introduction - What is SFEOS?](#project-introduction---what-is-sfeos)
|
|
108
108
|
- [Common Deployment Patterns](#common-deployment-patterns)
|
|
109
109
|
- [Technologies](#technologies)
|
|
110
110
|
- [Table of Contents](#table-of-contents)
|
|
111
111
|
- [Collection Search Extensions](#collection-search-extensions)
|
|
112
|
-
- [Documentation
|
|
112
|
+
- [Documentation & Resources](#documentation--resources)
|
|
113
113
|
- [Package Structure](#package-structure)
|
|
114
114
|
- [Examples](#examples)
|
|
115
115
|
- [Performance](#performance)
|
|
@@ -152,7 +152,11 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
|
|
|
152
152
|
|
|
153
153
|
## Collection Search Extensions
|
|
154
154
|
|
|
155
|
-
SFEOS
|
|
155
|
+
SFEOS provides enhanced collection search capabilities through two primary routes:
|
|
156
|
+
- **GET/POST `/collections`**: The standard STAC endpoint with extended query parameters
|
|
157
|
+
- **GET/POST `/collections-search`**: A custom endpoint that supports the same parameters, created to avoid conflicts with the STAC Transactions extension if enabled (which uses POST `/collections` for collection creation)
|
|
158
|
+
|
|
159
|
+
These endpoints support advanced collection discovery features including:
|
|
156
160
|
|
|
157
161
|
- **Sorting**: Sort collections by sortable fields using the `sortby` parameter
|
|
158
162
|
- Example: `/collections?sortby=+id` (ascending sort by ID)
|
|
@@ -168,9 +172,26 @@ SFEOS implements extended capabilities for the `/collections` endpoint, allowing
|
|
|
168
172
|
- Searches across multiple text fields including title, description, and keywords
|
|
169
173
|
- Supports partial word matching and relevance-based sorting
|
|
170
174
|
|
|
175
|
+
- **Structured Filtering**: Filter collections using CQL2 expressions
|
|
176
|
+
- JSON format: `/collections?filter={"op":"=","args":[{"property":"id"},"sentinel-2"]}&filter-lang=cql2-json`
|
|
177
|
+
- Text format: `/collections?filter=id='sentinel-2'&filter-lang=cql2-text` (note: string values must be quoted)
|
|
178
|
+
- Advanced text format: `/collections?filter=id LIKE '%sentinel%'&filter-lang=cql2-text` (supports LIKE, BETWEEN, etc.)
|
|
179
|
+
- Supports both CQL2 JSON and CQL2 text formats with various operators
|
|
180
|
+
- Enables precise filtering on any collection property
|
|
181
|
+
|
|
182
|
+
- **Datetime Filtering**: Filter collections by their temporal extent using the `datetime` parameter
|
|
183
|
+
- Example: `/collections?datetime=2020-01-01T00:00:00Z/2020-12-31T23:59:59Z` (finds collections with temporal extents that overlap this range)
|
|
184
|
+
- Example: `/collections?datetime=2020-06-15T12:00:00Z` (finds collections whose temporal extent includes this specific time)
|
|
185
|
+
- Example: `/collections?datetime=2020-01-01T00:00:00Z/..` (finds collections with temporal extents that extend to or beyond January 1, 2020)
|
|
186
|
+
- Example: `/collections?datetime=../2020-12-31T23:59:59Z` (finds collections with temporal extents that begin on or before December 31, 2020)
|
|
187
|
+
- Collections are matched if their temporal extent overlaps with the provided datetime parameter
|
|
188
|
+
- This allows for efficient discovery of collections based on time periods
|
|
189
|
+
|
|
171
190
|
These extensions make it easier to build user interfaces that display and navigate through collections efficiently.
|
|
172
191
|
|
|
173
|
-
> **Configuration**: Collection search extensions can be disabled by setting the `ENABLE_COLLECTIONS_SEARCH` environment variable to `false`. By default, these extensions are enabled.
|
|
192
|
+
> **Configuration**: Collection search extensions (sorting, field selection, free text search, structured filtering, and datetime filtering) for the `/collections` endpoint can be disabled by setting the `ENABLE_COLLECTIONS_SEARCH` environment variable to `false`. By default, these extensions are enabled.
|
|
193
|
+
>
|
|
194
|
+
> **Configuration**: The custom `/collections-search` endpoint can be enabled by setting the `ENABLE_COLLECTIONS_SEARCH_ROUTE` environment variable to `true`. By default, this endpoint is **disabled**.
|
|
174
195
|
|
|
175
196
|
> **Note**: Sorting is only available on fields that are indexed for sorting in Elasticsearch/OpenSearch. With the default mappings, you can sort on:
|
|
176
197
|
> - `id` (keyword field)
|
|
@@ -181,6 +202,7 @@ These extensions make it easier to build user interfaces that display and naviga
|
|
|
181
202
|
>
|
|
182
203
|
> **Important**: Adding keyword fields to make text fields sortable can significantly increase the index size, especially for large text fields. Consider the storage implications when deciding which fields to make sortable.
|
|
183
204
|
|
|
205
|
+
|
|
184
206
|
## Package Structure
|
|
185
207
|
|
|
186
208
|
This project is organized into several packages, each with a specific purpose:
|
|
@@ -193,7 +215,7 @@ This project is organized into several packages, each with a specific purpose:
|
|
|
193
215
|
- Shared logic and utilities that improve code reuse between backends
|
|
194
216
|
|
|
195
217
|
- **stac_fastapi_elasticsearch**: Complete implementation of the STAC API using Elasticsearch as the backend database. This package depends on both `stac_fastapi_core` and `sfeos_helpers`.
|
|
196
|
-
|
|
218
|
+
|
|
197
219
|
- **stac_fastapi_opensearch**: Complete implementation of the STAC API using OpenSearch as the backend database. This package depends on both `stac_fastapi_core` and `sfeos_helpers`.
|
|
198
220
|
|
|
199
221
|
## Examples
|
|
@@ -311,12 +333,13 @@ You can customize additional settings in your `.env` file:
|
|
|
311
333
|
| `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
|
|
312
334
|
| `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` | Optional |
|
|
313
335
|
| `DATABASE_REFRESH` | Controls 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. | `false` | Optional |
|
|
314
|
-
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields).
|
|
315
|
-
| `
|
|
336
|
+
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
|
|
337
|
+
| `ENABLE_COLLECTIONS_SEARCH_ROUTE` | Enable the custom `/collections-search` endpoint (both GET and POST methods). When disabled, the custom endpoint will not be available, but collection search extensions will still be available on the core `/collections` endpoint if `ENABLE_COLLECTIONS_SEARCH` is true. | `false` | Optional |
|
|
338
|
+
| `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. This is useful for deployments where mutating the catalog via the API should be prevented. If set to `true`, the POST `/collections` route for search will be unavailable in the API. | `true` | Optional |
|
|
316
339
|
| `STAC_ITEM_LIMIT` | Sets the environment variable for result limiting to SFEOS for the number of returned items and STAC collections. | `10` | Optional |
|
|
317
340
|
| `STAC_INDEX_ASSETS` | Controls if Assets are indexed when added to Elasticsearch/Opensearch. This allows asset fields to be included in search queries. | `false` | Optional |
|
|
318
341
|
| `ENV_MAX_LIMIT` | Configures the environment variable in SFEOS to override the default `MAX_LIMIT`, which controls the limit parameter for returned items and STAC collections. | `10,000` | Optional |
|
|
319
|
-
| `USE_DATETIME` | Configures the datetime search behavior in SFEOS. When enabled, searches both datetime field and falls back to start_datetime/end_datetime range for items with null datetime. When disabled, searches only by start_datetime/end_datetime range. |
|
|
342
|
+
| `USE_DATETIME` | Configures the datetime search behavior in SFEOS. When enabled, searches both datetime field and falls back to start_datetime/end_datetime range for items with null datetime. When disabled, searches only by start_datetime/end_datetime range. | `true` | Optional |
|
|
320
343
|
|
|
321
344
|
> [!NOTE]
|
|
322
345
|
> The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, `ES_VERIFY_CERTS` and `ES_TIMEOUT` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
|
|
@@ -462,7 +485,6 @@ The system uses a precise naming convention:
|
|
|
462
485
|
- `ENABLE_COLLECTIONS_SEARCH`: Set to `true` (default) to enable collection search extensions (sort, fields). Set to `false` to disable.
|
|
463
486
|
- `ENABLE_TRANSACTIONS_EXTENSIONS`: Set to `true` (default) to enable transaction extensions. Set to `false` to disable.
|
|
464
487
|
|
|
465
|
-
|
|
466
488
|
## Collection Pagination
|
|
467
489
|
|
|
468
490
|
- **Overview**: The collections route supports pagination through optional query parameters.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
stac_fastapi/opensearch/__init__.py,sha256=iJWMUgn7mUvmuPQSO_FlyhJ5eDdbbfmGv1qnFOX5-qk,28
|
|
2
|
+
stac_fastapi/opensearch/app.py,sha256=paRSzdkT3yxkegC3KEg98CA7PpNQ0C2LW0Mozb_LcP0,10025
|
|
3
|
+
stac_fastapi/opensearch/config.py,sha256=zGx4-4c5zEnu_Bh8Ra3SkIC83tluDiz-wKYQclRRDJA,5179
|
|
4
|
+
stac_fastapi/opensearch/database_logic.py,sha256=WzVor77OGJrMhBM-LSwUc5cKjKtlTEuxsuzS_OzfYv4,67817
|
|
5
|
+
stac_fastapi/opensearch/version.py,sha256=FuGC3fKnAmD4Wk95swJ6qCVBs5mZiShrlRKuSH-voyE,45
|
|
6
|
+
stac_fastapi_opensearch-6.5.1.dist-info/METADATA,sha256=i4JZbltYaOznnduQjNKH_PNqKcnzE7d_DITxqNHOuMQ,42048
|
|
7
|
+
stac_fastapi_opensearch-6.5.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
8
|
+
stac_fastapi_opensearch-6.5.1.dist-info/entry_points.txt,sha256=zjZ0Xsr9BUNJqMkdPpl6zEIUykv1uFdJtNELFRChp0w,76
|
|
9
|
+
stac_fastapi_opensearch-6.5.1.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
|
|
10
|
+
stac_fastapi_opensearch-6.5.1.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
stac_fastapi/opensearch/__init__.py,sha256=iJWMUgn7mUvmuPQSO_FlyhJ5eDdbbfmGv1qnFOX5-qk,28
|
|
2
|
-
stac_fastapi/opensearch/app.py,sha256=fd9S10gFUHG5GkuA8c83wYEOet-USNKu-B-p26l6glY,7485
|
|
3
|
-
stac_fastapi/opensearch/config.py,sha256=zGx4-4c5zEnu_Bh8Ra3SkIC83tluDiz-wKYQclRRDJA,5179
|
|
4
|
-
stac_fastapi/opensearch/database_logic.py,sha256=l7f__kvTUJlvoIGPxdYdhXkT3dZ_YwPj-kNCY1FC-Os,61259
|
|
5
|
-
stac_fastapi/opensearch/version.py,sha256=1xfRb7s1Stv055oEewNJpZn9OQUUcaJvgSzCsYLHYSQ,45
|
|
6
|
-
stac_fastapi_opensearch-6.4.0.dist-info/METADATA,sha256=lwQchY6F_5gNl9eWmwPRmnSJYQ0OWFHgcX1Hg5oboDM,39650
|
|
7
|
-
stac_fastapi_opensearch-6.4.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
8
|
-
stac_fastapi_opensearch-6.4.0.dist-info/entry_points.txt,sha256=zjZ0Xsr9BUNJqMkdPpl6zEIUykv1uFdJtNELFRChp0w,76
|
|
9
|
-
stac_fastapi_opensearch-6.4.0.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
|
|
10
|
-
stac_fastapi_opensearch-6.4.0.dist-info/RECORD,,
|
|
File without changes
|
{stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{stac_fastapi_opensearch-6.4.0.dist-info → stac_fastapi_opensearch-6.5.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|