From 5a66c6424679e7c7d2e4363169b49e4f2924ad45 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 12:01:31 +0800 Subject: [PATCH 1/8] update changelog, fixes --- CHANGELOG.md | 3 + .../core/extensions/collections_search.py | 62 ++++++++++++++++++- .../stac_fastapi/elasticsearch/app.py | 1 + 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e385bcee..01ea1fb83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,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. +- Fixed `q` parameter in GET `/collections-search` endpoint to be converted to a list format, matching the behavior of the `/collections` endpoint for consistency. + ### Removed ### Updated 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 0a3a0635c..5593efc50 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,50 @@ 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) + # But first, ensure the q parameter is properly handled as a list + # since FastAPI extracts it from the URL when it's defined as a function parameter + + # 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 + + # Temporarily replace request.query_params + original_query_params = request.query_params + request.query_params = query_params + + try: + return await original_endpoint(request) + finally: + # Restore original query_params + request.query_params = original_query_params documented_endpoint.__name__ = original_endpoint.__name__ return documented_endpoint @@ -95,6 +138,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 +150,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 +293,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/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 6012c1906..bcb870f39 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) From 6dde14351c1d74394ccdbb84701762fff6a1e448 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 12:12:47 +0800 Subject: [PATCH 2/8] use request wrapper --- .../core/extensions/collections_search.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) 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 5593efc50..4a8328057 100644 --- a/stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py +++ b/stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py @@ -93,9 +93,9 @@ async def documented_endpoint( example="cql2-json", ), ): - # Delegate to original endpoint which reads from request.query_params - # But first, ensure the q parameter is properly handled as a list - # since FastAPI extracts it from the URL when it's defined as a function parameter + # 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"): @@ -113,15 +113,28 @@ async def documented_endpoint( # Already a list, use as-is query_params["q"] = q - # Temporarily replace request.query_params - original_query_params = request.query_params - request.query_params = query_params + # 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 - try: - return await original_endpoint(request) - finally: - # Restore original query_params - request.query_params = original_query_params + # 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 From 2b92cb87af9ceb6f9e1baeb16ea82a42e3688136 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 12:17:47 +0800 Subject: [PATCH 3/8] fix test prefix --- stac_fastapi/tests/api/test_api_search_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/tests/api/test_api_search_collections.py b/stac_fastapi/tests/api/test_api_search_collections.py index 19c9c6071..ddb46ae00 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() From a002b70b5d1c393c3b94b9d13a64f351e4ea8094 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 12:23:51 +0800 Subject: [PATCH 4/8] change like pattern --- .../sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py index bd248c90b..e6971f6a7 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py @@ -2,7 +2,7 @@ import re -cql2_like_patterns = re.compile(r"\\.|[%_]|\\$") +cql2_like_patterns = re.compile(r"\\\\|\\%|\\_|[%_]") valid_like_substitutions = { "\\\\": "\\", "\\%": "%", From f49c75d4043e5ee3484ca1d564ad23b2a6b0c405 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 12:28:30 +0800 Subject: [PATCH 5/8] extend like patterns --- .../sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py index e6971f6a7..cada030ea 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py @@ -2,7 +2,7 @@ import re -cql2_like_patterns = re.compile(r"\\\\|\\%|\\_|[%_]") +cql2_like_patterns = re.compile(r"\\\\|\\%|\\_|\\[^%_\\]|[%_]") valid_like_substitutions = { "\\\\": "\\", "\\%": "%", From c25b2a46c9addee1a6f55955732244e66c2f8844 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 12:37:55 +0800 Subject: [PATCH 6/8] revert pattern --- .../sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py | 2 +- stac_fastapi/tests/api/test_api_search_collections.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py index cada030ea..bd248c90b 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/cql2.py @@ -2,7 +2,7 @@ import re -cql2_like_patterns = re.compile(r"\\\\|\\%|\\_|\\[^%_\\]|[%_]") +cql2_like_patterns = re.compile(r"\\.|[%_]|\\$") valid_like_substitutions = { "\\\\": "\\", "\\%": "%", diff --git a/stac_fastapi/tests/api/test_api_search_collections.py b/stac_fastapi/tests/api/test_api_search_collections.py index ddb46ae00..0bdbe1793 100644 --- a/stac_fastapi/tests/api/test_api_search_collections.py +++ b/stac_fastapi/tests/api/test_api_search_collections.py @@ -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() From b0d293ff21d5997e3787b4776edd7970a579a07b Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 14:17:44 +0800 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ea1fb83..00b77c80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ 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. -- Fixed `q` parameter in GET `/collections-search` endpoint to be converted to a list format, matching the behavior of the `/collections` endpoint for consistency. +- 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 From f6344188ec244c149652d5a492647e431f5be21b Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Mon, 27 Oct 2025 14:29:25 +0800 Subject: [PATCH 8/8] release v6.7.0 --- CHANGELOG.md | 15 ++++++++++++++- stac_fastapi/core/stac_fastapi/core/version.py | 2 +- stac_fastapi/elasticsearch/pyproject.toml | 4 ++-- .../stac_fastapi/elasticsearch/version.py | 2 +- stac_fastapi/opensearch/pyproject.toml | 4 ++-- .../opensearch/stac_fastapi/opensearch/version.py | 2 +- stac_fastapi/sfeos_helpers/pyproject.toml | 2 +- .../stac_fastapi/sfeos_helpers/version.py | 2 +- 8 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b77c80e..49ec8c12a 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) @@ -597,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/version.py b/stac_fastapi/core/stac_fastapi/core/version.py index 1335b265b..aec049d8b 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 340f9bf5c..5a510fa46 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/version.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py index 1335b265b..aec049d8b 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 24f7fb09a..f7dff98e4 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 1335b265b..aec049d8b 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 4c49aec11..28c87ced8 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 1335b265b..aec049d8b 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"