Skip to content

Commit 8380854

Browse files
authored
Merge branch 'main' into release/v6.5.0
2 parents c46cbf9 + f85eb7a commit 8380854

File tree

12 files changed

+1061
-171
lines changed

12 files changed

+1061
-171
lines changed

CHANGELOG.md

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

1818
### Added
1919

20+
- Environment variable `ENABLE_COLLECTIONS_SEARCH_ROUTE` to turn on/off the `/collections-search` endpoint. [#478](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/478)
21+
- POST and GET `/collections-search` endpoint for collections search queries, needed because POST /collections search will not work when the Transactions Extension is enabled. Defaults to `False` [#478](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/478)
2022
- 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)
2123
- GET `/collections` collection search query extension. [#477](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/477)
2224
- GET `/collections` collections search datetime filtering support. [#476](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/476)
2325

2426
### Changed
27+
- Refactored `/collections` endpoint implementation to support both GET and POST methods. [#478](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/478)
2528

2629
### Fixed
27-
2830
- support of disabled nested attributes in the properties dictionary. [#474](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/474)
2931

3032
## [v6.4.0] - 2025-09-24

README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
6666
## Table of Contents
6767

6868
- [stac-fastapi-elasticsearch-opensearch](#stac-fastapi-elasticsearch-opensearch)
69-
- [Sponsors \& Supporters](#sponsors--supporters)
69+
- [Sponsors & Supporters](#sponsors--supporters)
7070
- [Project Introduction - What is SFEOS?](#project-introduction---what-is-sfeos)
7171
- [Common Deployment Patterns](#common-deployment-patterns)
7272
- [Technologies](#technologies)
7373
- [Table of Contents](#table-of-contents)
7474
- [Collection Search Extensions](#collection-search-extensions)
75-
- [Documentation \& Resources](#documentation--resources)
75+
- [Documentation & Resources](#documentation--resources)
7676
- [Package Structure](#package-structure)
7777
- [Examples](#examples)
7878
- [Performance](#performance)
@@ -115,7 +115,11 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
115115

116116
## Collection Search Extensions
117117

118-
SFEOS implements extended capabilities for the `/collections` endpoint, allowing for more powerful collection discovery:
118+
SFEOS provides enhanced collection search capabilities through two primary routes:
119+
- **GET/POST `/collections`**: The standard STAC endpoint with extended query parameters
120+
- **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)
121+
122+
These endpoints support advanced collection discovery features including:
119123

120124
- **Sorting**: Sort collections by sortable fields using the `sortby` parameter
121125
- Example: `/collections?sortby=+id` (ascending sort by ID)
@@ -146,11 +150,11 @@ SFEOS implements extended capabilities for the `/collections` endpoint, allowing
146150
- Collections are matched if their temporal extent overlaps with the provided datetime parameter
147151
- This allows for efficient discovery of collections based on time periods
148152

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.
150-
151153
These extensions make it easier to build user interfaces that display and navigate through collections efficiently.
152154

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.
155+
> **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.
156+
>
157+
> **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**.
154158
155159
> **Note**: Sorting is only available on fields that are indexed for sorting in Elasticsearch/OpenSearch. With the default mappings, you can sort on:
156160
> - `id` (keyword field)
@@ -161,6 +165,7 @@ These extensions make it easier to build user interfaces that display and naviga
161165
>
162166
> **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.
163167
168+
164169
## Package Structure
165170

166171
This project is organized into several packages, each with a specific purpose:
@@ -291,8 +296,9 @@ You can customize additional settings in your `.env` file:
291296
| `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
292297
| `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 |
293298
| `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 |
294-
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering). | `true` | Optional |
295-
| `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 |
299+
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
300+
| `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 |
301+
| `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 |
296302
| `STAC_ITEM_LIMIT` | Sets the environment variable for result limiting to SFEOS for the number of returned items and STAC collections. | `10` | Optional |
297303
| `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 |
298304
| `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 |
@@ -442,7 +448,6 @@ The system uses a precise naming convention:
442448
- `ENABLE_COLLECTIONS_SEARCH`: Set to `true` (default) to enable collection search extensions (sort, fields). Set to `false` to disable.
443449
- `ENABLE_TRANSACTIONS_EXTENSIONS`: Set to `true` (default) to enable transaction extensions. Set to `false` to disable.
444450

445-
446451
## Collection Pagination
447452

448453
- **Overview**: The collections route supports pagination through optional query parameters.

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,20 @@ def _landing_page(
136136
"href": urljoin(base_url, "search"),
137137
"method": "POST",
138138
},
139+
{
140+
"rel": "collections-search",
141+
"type": "application/json",
142+
"title": "Collections Search",
143+
"href": urljoin(base_url, "collections-search"),
144+
"method": "GET",
145+
},
146+
{
147+
"rel": "collections-search",
148+
"type": "application/json",
149+
"title": "Collections Search",
150+
"href": urljoin(base_url, "collections-search"),
151+
"method": "POST",
152+
},
139153
],
140154
stac_extensions=extension_schemas,
141155
)
@@ -227,8 +241,9 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
227241
async def all_collections(
228242
self,
229243
datetime: Optional[str] = None,
244+
limit: Optional[int] = None,
230245
fields: Optional[List[str]] = None,
231-
sortby: Optional[str] = None,
246+
sortby: Optional[Union[str, List[str]]] = None,
232247
filter_expr: Optional[str] = None,
233248
filter_lang: Optional[str] = None,
234249
q: Optional[Union[str, List[str]]] = None,
@@ -239,6 +254,7 @@ async def all_collections(
239254
240255
Args:
241256
datetime (Optional[str]): Filter collections by datetime range.
257+
limit (Optional[int]): Maximum number of collections to return.
242258
fields (Optional[List[str]]): Fields to include or exclude from the results.
243259
sortby (Optional[str]): Sorting options for the results.
244260
filter_expr (Optional[str]): Structured filter expression in CQL2 JSON or CQL2-text format.
@@ -252,7 +268,36 @@ async def all_collections(
252268
"""
253269
request = kwargs["request"]
254270
base_url = str(request.base_url)
255-
limit = int(request.query_params.get("limit", os.getenv("STAC_ITEM_LIMIT", 10)))
271+
272+
# Get the global limit from environment variable
273+
global_limit = None
274+
env_limit = os.getenv("STAC_ITEM_LIMIT")
275+
if env_limit:
276+
try:
277+
global_limit = int(env_limit)
278+
except ValueError:
279+
# Handle invalid integer in environment variable
280+
pass
281+
282+
# Apply global limit if it exists
283+
if global_limit is not None:
284+
# If a limit was provided, use the smaller of the two
285+
if limit is not None:
286+
limit = min(limit, global_limit)
287+
else:
288+
limit = global_limit
289+
else:
290+
# No global limit, use provided limit or default
291+
if limit is None:
292+
query_limit = request.query_params.get("limit")
293+
if query_limit:
294+
try:
295+
limit = int(query_limit)
296+
except ValueError:
297+
limit = 10
298+
else:
299+
limit = 10
300+
256301
token = request.query_params.get("token")
257302

258303
# Process fields parameter for filtering collection properties
@@ -262,7 +307,8 @@ async def all_collections(
262307
if field[0] == "-":
263308
excludes.add(field[1:])
264309
else:
265-
includes.add(field[1:] if field[0] in "+ " else field)
310+
include_field = field[1:] if field[0] in "+ " else field
311+
includes.add(include_field)
266312

267313
sort = None
268314
if sortby:
@@ -337,6 +383,7 @@ async def all_collections(
337383
raise HTTPException(
338384
status_code=400, detail=f"Error parsing filter: {e}"
339385
)
386+
340387
except Exception as e:
341388
raise HTTPException(
342389
status_code=400, detail=f"Invalid filter parameter: {e}"
@@ -346,7 +393,7 @@ async def all_collections(
346393
if datetime:
347394
parsed_datetime = format_datetime_range(date_str=datetime)
348395

349-
collections, next_token = await self.database.get_all_collections(
396+
collections, next_token, maybe_count = await self.database.get_all_collections(
350397
token=token,
351398
limit=limit,
352399
request=request,
@@ -380,7 +427,91 @@ async def all_collections(
380427
next_link = PagingLinks(next=next_token, request=request).link_next()
381428
links.append(next_link)
382429

383-
return stac_types.Collections(collections=filtered_collections, links=links)
430+
return stac_types.Collections(
431+
collections=filtered_collections,
432+
links=links,
433+
numberMatched=maybe_count,
434+
numberReturned=len(filtered_collections),
435+
)
436+
437+
async def post_all_collections(
438+
self, search_request: BaseSearchPostRequest, request: Request, **kwargs
439+
) -> stac_types.Collections:
440+
"""Search collections with POST request.
441+
442+
Args:
443+
search_request (BaseSearchPostRequest): The search request.
444+
request (Request): The request.
445+
446+
Returns:
447+
A Collections object containing all the collections in the database and links to various resources.
448+
"""
449+
request.postbody = search_request.model_dump(exclude_unset=True)
450+
451+
fields = None
452+
453+
# Check for field attribute (ExtendedSearch format)
454+
if hasattr(search_request, "field") and search_request.field:
455+
fields = []
456+
457+
# Handle include fields
458+
if (
459+
hasattr(search_request.field, "includes")
460+
and search_request.field.includes
461+
):
462+
for field in search_request.field.includes:
463+
fields.append(f"+{field}")
464+
465+
# Handle exclude fields
466+
if (
467+
hasattr(search_request.field, "excludes")
468+
and search_request.field.excludes
469+
):
470+
for field in search_request.field.excludes:
471+
fields.append(f"-{field}")
472+
473+
# Convert sortby parameter from POST format to all_collections format
474+
sortby = None
475+
# Check for sortby attribute
476+
if hasattr(search_request, "sortby") and search_request.sortby:
477+
# Create a list of sort strings in the format expected by all_collections
478+
sortby = []
479+
for sort_item in search_request.sortby:
480+
# Handle different types of sort items
481+
if hasattr(sort_item, "field") and hasattr(sort_item, "direction"):
482+
# This is a Pydantic model with field and direction attributes
483+
field = sort_item.field
484+
direction = sort_item.direction
485+
elif isinstance(sort_item, dict):
486+
# This is a dictionary with field and direction keys
487+
field = sort_item.get("field")
488+
direction = sort_item.get("direction", "asc")
489+
else:
490+
# Skip this item if we can't extract field and direction
491+
continue
492+
493+
if field:
494+
# Create a sort string in the format expected by all_collections
495+
# e.g., "-id" for descending sort on id field
496+
prefix = "-" if direction.lower() == "desc" else ""
497+
sortby.append(f"{prefix}{field}")
498+
499+
# Pass all parameters from search_request to all_collections
500+
return await self.all_collections(
501+
limit=search_request.limit if hasattr(search_request, "limit") else None,
502+
fields=fields,
503+
sortby=sortby,
504+
filter_expr=search_request.filter
505+
if hasattr(search_request, "filter")
506+
else None,
507+
filter_lang=search_request.filter_lang
508+
if hasattr(search_request, "filter_lang")
509+
else None,
510+
query=search_request.query if hasattr(search_request, "query") else None,
511+
q=search_request.q if hasattr(search_request, "q") else None,
512+
request=request,
513+
**kwargs,
514+
)
384515

385516
async def get_collection(
386517
self, collection_id: str, **kwargs
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
"""elasticsearch extensions modifications."""
22

3+
from .collections_search import CollectionsSearchEndpointExtension
34
from .query import Operator, QueryableTypes, QueryExtension
45

5-
__all__ = ["Operator", "QueryableTypes", "QueryExtension"]
6+
__all__ = [
7+
"Operator",
8+
"QueryableTypes",
9+
"QueryExtension",
10+
"CollectionsSearchEndpointExtension",
11+
]

0 commit comments

Comments
 (0)