diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e385bce..49ec8c12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +### Changed + +### Fixed + +### Removed + +### Updated + +## [v6.7.0] - 2025-10-27 + +### Added + - Environment variable `EXCLUDED_FROM_QUERYABLES` to exclude specific fields from queryables endpoint and filtering. Supports comma-separated list of fully qualified field names (e.g., `properties.auth:schemes,properties.storage:schemes`) [#489](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/489) - Added Redis caching configuration for navigation pagination support, enabling proper `prev` and `next` links in paginated responses. [#488](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/488) @@ -16,6 +28,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed +- Fixed filter parameter handling for GET `/collections-search` endpoint. Filter parameters (`filter` and `filter-lang`) are now properly passed through and processed. [#511](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/511) +- Fixed `q` parameter in GET `/collections-search` endpoint to be converted to a list format, matching the behavior of the `/collections` endpoint for consistency. [#511](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/511) + ### Removed ### Updated @@ -594,7 +609,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Use genexp in execute_search and get_all_collections to return results. - Added db_to_stac serializer to item_collection method in core.py. -[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.6.0...main +[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.0...main +[v6.7.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.6.0...v6.7.0 [v6.6.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.5.1...v6.6.0 [v6.5.1]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.5.0...v6.5.1 [v6.5.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.4.0...v6.5.0 diff --git a/stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py b/stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py index 0a3a0635..4a832805 100644 --- a/stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py +++ b/stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py @@ -22,6 +22,8 @@ class CollectionsSearchRequest(ExtendedSearch): query: Optional[ str ] = None # Legacy query extension (deprecated but still supported) + filter_expr: Optional[str] = None + filter_lang: Optional[str] = None def build_get_collections_search_doc(original_endpoint): @@ -29,7 +31,7 @@ def build_get_collections_search_doc(original_endpoint): async def documented_endpoint( request: Request, - q: Optional[str] = Query( + q: Optional[Union[str, List[str]]] = Query( None, description="Free text search query", ), @@ -76,9 +78,63 @@ async def documented_endpoint( ), alias="fields[]", ), + filter: Optional[str] = Query( + None, + description=( + "Structured filter expression in CQL2 JSON or CQL2-text format" + ), + example='{"op": "=", "args": [{"property": "properties.category"}, "level2"]}', + ), + filter_lang: Optional[str] = Query( + None, + description=( + "Filter language. Must be 'cql2-json' or 'cql2-text' if specified" + ), + example="cql2-json", + ), ): - # Delegate to original endpoint which reads from request.query_params - return await original_endpoint(request) + # Delegate to original endpoint with parameters + # Since FastAPI extracts parameters from the URL when they're defined as function parameters, + # we need to create a request wrapper that provides our modified query_params + + # Create a mutable copy of query_params + if hasattr(request, "_query_params"): + query_params = dict(request._query_params) + else: + query_params = dict(request.query_params) + + # Add q parameter back to query_params if it was provided + # Convert to list format to match /collections behavior + if q is not None: + if isinstance(q, str): + # Single string should become a list with one element + query_params["q"] = [q] + elif isinstance(q, list): + # Already a list, use as-is + query_params["q"] = q + + # Add filter parameters back to query_params if they were provided + if filter is not None: + query_params["filter"] = filter + if filter_lang is not None: + query_params["filter-lang"] = filter_lang + + # Create a request wrapper that provides our modified query_params + class RequestWrapper: + def __init__(self, original_request, modified_query_params): + self._original = original_request + self._query_params = modified_query_params + + @property + def query_params(self): + return self._query_params + + def __getattr__(self, name): + # Delegate all other attributes to the original request + return getattr(self._original, name) + + wrapped_request = RequestWrapper(request, query_params) + return await original_endpoint(wrapped_request) documented_endpoint.__name__ = original_endpoint.__name__ return documented_endpoint @@ -95,6 +151,8 @@ async def documented_post_endpoint( "Search parameters for collections.\n\n" "- `q`: Free text search query (string or list of strings)\n" "- `query`: Additional filtering expressed as a string (legacy support)\n" + "- `filter`: Structured filter expression in CQL2 JSON or CQL2-text format\n" + "- `filter_lang`: Filter language. Must be 'cql2-json' or 'cql2-text' if specified\n" "- `limit`: Maximum number of results to return (default: 10)\n" "- `token`: Pagination token for the next page of results\n" "- `bbox`: Bounding box [minx, miny, maxx, maxy] or [minx, miny, minz, maxx, maxy, maxz]\n" @@ -105,6 +163,11 @@ async def documented_post_endpoint( example={ "q": "landsat", "query": "platform=landsat AND collection_category=level2", + "filter": { + "op": "=", + "args": [{"property": "properties.category"}, "level2"], + }, + "filter_lang": "cql2-json", "limit": 10, "token": "next-page-token", "bbox": [-180, -90, 180, 90], @@ -243,6 +306,14 @@ async def collections_search_get_endpoint( sortby = sortby_str.split(",") params["sortby"] = sortby + # Handle filter parameter mapping (fixed for collections-search) + if "filter" in params: + params["filter_expr"] = params.pop("filter") + + # Handle filter-lang parameter mapping (fixed for collections-search) + if "filter-lang" in params: + params["filter_lang"] = params.pop("filter-lang") + collections = await self.client.all_collections(request=request, **params) return collections diff --git a/stac_fastapi/core/stac_fastapi/core/version.py b/stac_fastapi/core/stac_fastapi/core/version.py index 1335b265..aec049d8 100644 --- a/stac_fastapi/core/stac_fastapi/core/version.py +++ b/stac_fastapi/core/stac_fastapi/core/version.py @@ -1,2 +1,2 @@ """library version.""" -__version__ = "6.6.0" +__version__ = "6.7.0" diff --git a/stac_fastapi/elasticsearch/pyproject.toml b/stac_fastapi/elasticsearch/pyproject.toml index 340f9bf5..5a510fa4 100644 --- a/stac_fastapi/elasticsearch/pyproject.toml +++ b/stac_fastapi/elasticsearch/pyproject.toml @@ -30,8 +30,8 @@ keywords = [ ] dynamic = ["version"] dependencies = [ - "stac-fastapi-core==6.6.0", - "sfeos-helpers==6.6.0", + "stac-fastapi-core==6.7.0", + "sfeos-helpers==6.7.0", "elasticsearch[async]~=8.19.1", "uvicorn~=0.23.0", "starlette>=0.35.0,<0.36.0", diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 6012c190..bcb870f3 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -198,6 +198,7 @@ FieldsConformanceClasses.COLLECTIONS, ], ) + extensions.append(collection_search_ext) extensions.append(collections_search_endpoint_ext) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py index 1335b265..aec049d8 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py @@ -1,2 +1,2 @@ """library version.""" -__version__ = "6.6.0" +__version__ = "6.7.0" diff --git a/stac_fastapi/opensearch/pyproject.toml b/stac_fastapi/opensearch/pyproject.toml index 24f7fb09..f7dff98e 100644 --- a/stac_fastapi/opensearch/pyproject.toml +++ b/stac_fastapi/opensearch/pyproject.toml @@ -30,8 +30,8 @@ keywords = [ ] dynamic = ["version"] dependencies = [ - "stac-fastapi-core==6.6.0", - "sfeos-helpers==6.6.0", + "stac-fastapi-core==6.7.0", + "sfeos-helpers==6.7.0", "opensearch-py~=2.8.0", "opensearch-py[async]~=2.8.0", "uvicorn~=0.23.0", diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py index 1335b265..aec049d8 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py @@ -1,2 +1,2 @@ """library version.""" -__version__ = "6.6.0" +__version__ = "6.7.0" diff --git a/stac_fastapi/sfeos_helpers/pyproject.toml b/stac_fastapi/sfeos_helpers/pyproject.toml index 4c49aec1..28c87ced 100644 --- a/stac_fastapi/sfeos_helpers/pyproject.toml +++ b/stac_fastapi/sfeos_helpers/pyproject.toml @@ -31,7 +31,7 @@ keywords = [ ] dynamic = ["version"] dependencies = [ - "stac-fastapi.core==6.6.0", + "stac-fastapi.core==6.7.0", ] [project.urls] diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/version.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/version.py index 1335b265..aec049d8 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/version.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/version.py @@ -1,2 +1,2 @@ """library version.""" -__version__ = "6.6.0" +__version__ = "6.7.0" diff --git a/stac_fastapi/tests/api/test_api_search_collections.py b/stac_fastapi/tests/api/test_api_search_collections.py index 19c9c607..0bdbe179 100644 --- a/stac_fastapi/tests/api/test_api_search_collections.py +++ b/stac_fastapi/tests/api/test_api_search_collections.py @@ -829,7 +829,7 @@ async def test_collections_post(app_client, txn_client, ctx): async def test_collections_search_cql2_text(app_client, txn_client, ctx): """Test collections search with CQL2-text filter.""" # Create a unique prefix for test collections - test_prefix = f"test-{uuid.uuid4()}" + test_prefix = f"test-{uuid.uuid4().hex[:8]}" # Create test collections collection_data = ctx.collection.copy() @@ -855,9 +855,8 @@ async def test_collections_search_cql2_text(app_client, txn_client, ctx): assert filtered_collections[0]["id"] == collection_id # Test GET search with more complex CQL2-text filter (LIKE operator) - test_prefix_escaped = test_prefix.replace("-", "\\-") resp = await app_client.get( - f"/collections-search?filter-lang=cql2-text&filter=id LIKE '{test_prefix_escaped}%'" + f"/collections-search?filter-lang=cql2-text&filter=id LIKE '{test_prefix}%'" ) assert resp.status_code == 200 resp_json = resp.json()