Skip to content

Commit 638e6e3

Browse files
committed
update readme, opensearch
1 parent e269e60 commit 638e6e3

File tree

5 files changed

+66
-43
lines changed

5 files changed

+66
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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
### Added
1111

1212
- GET `/collections` collection search structured filter extension with support for both cql2-json and cql2-text formats. [#475](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/475)
13+
- GET `/collections` collections search datetime filtering support. [#476](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/476)
1314

1415
### Changed
1516

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,19 @@ SFEOS implements extended capabilities for the `/collections` endpoint, allowing
138138
- Supports both CQL2 JSON and CQL2 text formats with various operators
139139
- Enables precise filtering on any collection property
140140

141-
> **Note on HTTP Methods**: All collection search extensions (sorting, field selection, free text search, and structured filtering) currently only support GET requests. POST requests with these parameters in the request body are not yet supported.
141+
- **Datetime Filtering**: Filter collections by their temporal extent using the `datetime` parameter
142+
- Example: `/collections?datetime=2020-01-01T00:00:00Z/2020-12-31T23:59:59Z` (finds collections with temporal extents that overlap this range)
143+
- Example: `/collections?datetime=2020-06-15T12:00:00Z` (finds collections whose temporal extent includes this specific time)
144+
- Example: `/collections?datetime=2020-01-01T00:00:00Z/..` (finds collections with temporal extents that extend to or beyond January 1, 2020)
145+
- Example: `/collections?datetime=../2020-12-31T23:59:59Z` (finds collections with temporal extents that begin on or before December 31, 2020)
146+
- Collections are matched if their temporal extent overlaps with the provided datetime parameter
147+
- This allows for efficient discovery of collections based on time periods
148+
149+
> **Note on HTTP Methods**: All collection search extensions (sorting, field selection, free text search, structured filtering, and datetime filtering) currently only support GET requests. POST requests with these parameters in the request body are not yet supported.
142150
143151
These extensions make it easier to build user interfaces that display and navigate through collections efficiently.
144152

145-
> **Configuration**: Collection search extensions (sorting, field selection, free text search, and structured filtering) can be disabled by setting the `ENABLE_COLLECTIONS_SEARCH` environment variable to `false`. By default, these extensions are enabled.
153+
> **Configuration**: Collection search extensions (sorting, field selection, free text search, structured filtering, and datetime filtering) can be disabled by setting the `ENABLE_COLLECTIONS_SEARCH` environment variable to `false`. By default, these extensions are enabled.
146154
147155
> **Note**: Sorting is only available on fields that are indexed for sorting in Elasticsearch/OpenSearch. With the default mappings, you can sort on:
148156
> - `id` (keyword field)
@@ -283,12 +291,12 @@ You can customize additional settings in your `.env` file:
283291
| `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
284292
| `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 |
285293
| `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 |
286-
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields). | `true` | Optional |
294+
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering). | `true` | Optional |
287295
| `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. If set to `false`, the POST `/collections` route and related transaction endpoints (including bulk transaction operations) will be unavailable in the API. This is useful for deployments where mutating the catalog via the API should be prevented. | `true` | Optional |
288296
| `STAC_ITEM_LIMIT` | Sets the environment variable for result limiting to SFEOS for the number of returned items and STAC collections. | `10` | Optional |
289297
| `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 |
290298
| `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 |
291-
| `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 |
299+
| `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. This applies to both item searches and collection searches. | `true` | Optional |
292300

293301
> [!NOTE]
294302
> 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.

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ async def get_all_collections(
188188
sort (Optional[List[Dict[str, Any]]]): Optional sort parameter from the request.
189189
q (Optional[List[str]]): Free text search terms.
190190
filter (Optional[Dict[str, Any]]): Structured query in CQL2 format.
191+
datetime (Optional[str]): Temporal filter.
191192
192193
Returns:
193194
A tuple of (collections, next pagination token if any).
@@ -271,16 +272,12 @@ async def get_all_collections(
271272
es_query = filter_module.to_es(await self.get_queryables_mapping(), filter)
272273
query_parts.append(es_query)
273274

274-
print("datetime: ", datetime)
275-
print("type datetime, ", type(datetime))
276275
datetime_filter = None
277276
if datetime:
278277
datetime_filter = self._apply_collection_datetime_filter(datetime)
279278
if datetime_filter:
280279
query_parts.append(datetime_filter)
281280

282-
print("datetime filter: ", datetime_filter)
283-
284281
# Combine all query parts with AND logic
285282
if query_parts:
286283
body["query"] = (
@@ -333,18 +330,6 @@ def _apply_collection_datetime_filter(
333330
# If it's just a single date, use it for both start and end
334331
start = end = datetime_str
335332

336-
# For a collection with temporal extent [start_date, end_date],
337-
# a datetime query should match if the datetime falls within the range.
338-
# For a date range query, it should match if the ranges overlap.
339-
340-
# For collections, we need a different approach because the temporal extent
341-
# is stored as an array of dates, not as a range field.
342-
# We need to check if:
343-
# 1. The collection's start date is before or equal to the query end date
344-
# 2. The collection's end date is after or equal to the query start date
345-
346-
# This is a bit tricky with Elasticsearch's flattened arrays, but we can use
347-
# a bool query to check both conditions
348333
return {
349334
"bool": {
350335
"must": [

stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ async def get_all_collections(
161161
sort: Optional[List[Dict[str, Any]]] = None,
162162
q: Optional[List[str]] = None,
163163
filter: Optional[Dict[str, Any]] = None,
164+
datetime: Optional[str] = None,
164165
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
165166
"""Retrieve a list of collections from Opensearch, supporting pagination.
166167
@@ -171,6 +172,7 @@ async def get_all_collections(
171172
sort (Optional[List[Dict[str, Any]]]): Optional sort parameter from the request.
172173
q (Optional[List[str]]): Free text search terms.
173174
filter (Optional[Dict[str, Any]]): Structured query in CQL2 format.
175+
datetime (Optional[str]): Temporal filter.
174176
175177
Returns:
176178
A tuple of (collections, next pagination token if any).
@@ -254,6 +256,12 @@ async def get_all_collections(
254256
es_query = filter_module.to_es(await self.get_queryables_mapping(), filter)
255257
query_parts.append(es_query)
256258

259+
datetime_filter = None
260+
if datetime:
261+
datetime_filter = self._apply_collection_datetime_filter(datetime)
262+
if datetime_filter:
263+
query_parts.append(datetime_filter)
264+
257265
# Combine all query parts with AND logic
258266
if query_parts:
259267
body["query"] = (
@@ -370,6 +378,41 @@ def apply_free_text_filter(search: Search, free_text_queries: Optional[List[str]
370378
search=search, free_text_queries=free_text_queries
371379
)
372380

381+
@staticmethod
382+
def _apply_collection_datetime_filter(
383+
datetime_str: Optional[str],
384+
) -> Optional[Dict[str, Any]]:
385+
"""Create a temporal filter for collections based on their extent."""
386+
if not datetime_str:
387+
return None
388+
389+
# Parse the datetime string into start and end
390+
if "/" in datetime_str:
391+
start, end = datetime_str.split("/")
392+
# Replace open-ended ranges with concrete dates
393+
if start == "..":
394+
# For open-ended start, use a very early date
395+
start = "1800-01-01T00:00:00Z"
396+
if end == "..":
397+
# For open-ended end, use a far future date
398+
end = "2999-12-31T23:59:59Z"
399+
else:
400+
# If it's just a single date, use it for both start and end
401+
start = end = datetime_str
402+
403+
return {
404+
"bool": {
405+
"must": [
406+
# Check if any date in the array is less than or equal to the query end date
407+
# This will match if the collection's start date is before or equal to the query end date
408+
{"range": {"extent.temporal.interval": {"lte": end}}},
409+
# Check if any date in the array is greater than or equal to the query start date
410+
# This will match if the collection's end date is after or equal to the query start date
411+
{"range": {"extent.temporal.interval": {"gte": start}}},
412+
]
413+
}
414+
}
415+
373416
@staticmethod
374417
def apply_datetime_filter(
375418
search: Search, datetime: Optional[str]

stac_fastapi/tests/api/test_api_search_collections.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -316,28 +316,18 @@ async def test_collections_filter_search(app_client, txn_client, load_test_data)
316316

317317

318318
@pytest.mark.asyncio
319-
async def test_collections_datetime_filter(app_client, load_test_data):
319+
async def test_collections_datetime_filter(app_client, load_test_data, txn_client):
320320
"""Test filtering collections by datetime."""
321321
# Create a test collection with a specific temporal extent
322-
test_collection_id = "test-collection-datetime"
323-
test_collection = {
324-
"id": test_collection_id,
325-
"type": "Collection",
326-
"stac_version": "1.0.0",
327-
"description": "Test collection for datetime filtering",
328-
"links": [],
329-
"extent": {
330-
"spatial": {"bbox": [[-180, -90, 180, 90]]},
331-
"temporal": {
332-
"interval": [["2020-01-01T00:00:00Z", "2020-12-31T23:59:59Z"]]
333-
},
334-
},
335-
"license": "proprietary",
336-
}
337322

338-
# Create the test collection
339-
resp = await app_client.post("/collections", json=test_collection)
340-
assert resp.status_code == 201
323+
base_collection = load_test_data("test_collection.json")
324+
base_collection["extent"]["temporal"]["interval"] = [
325+
["2020-01-01T00:00:00Z", "2020-12-31T23:59:59Z"]
326+
]
327+
test_collection_id = base_collection["id"]
328+
329+
await create_collection(txn_client, base_collection)
330+
await refresh_indices(txn_client)
341331

342332
# Test 1: Datetime range that overlaps with collection's temporal extent
343333
resp = await app_client.get(
@@ -413,7 +403,3 @@ async def test_collections_datetime_filter(app_client, load_test_data):
413403
found_collections = [c for c in resp_json["collections"] if c["id"] == test_collection_id]
414404
assert len(found_collections) == 1, f"Expected to find collection {test_collection_id} with open-ended past range to a date within its range"
415405
"""
416-
417-
# Clean up - delete the test collection
418-
resp = await app_client.delete(f"/collections/{test_collection_id}")
419-
assert resp.status_code == 204

0 commit comments

Comments
 (0)