From 824357cf1ee859f6564e6388bf06358e501a4843 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Wed, 4 Jun 2025 10:17:42 +0000 Subject: [PATCH 1/9] Remove unused Makefile vars --- Makefile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5896e734..c23ca951 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,11 @@ APP_HOST ?= 0.0.0.0 EXTERNAL_APP_PORT ?= 8080 ES_APP_PORT ?= 8080 +OS_APP_PORT ?= 8082 + ES_HOST ?= docker.for.mac.localhost ES_PORT ?= 9200 -OS_APP_PORT ?= 8082 -OS_HOST ?= docker.for.mac.localhost -OS_PORT ?= 9202 - run_es = docker compose \ run \ -p ${EXTERNAL_APP_PORT}:${ES_APP_PORT} \ From bc2930cccabee2fd0a7c01c9d3bdfe1fb0ac4146 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Wed, 4 Jun 2025 10:35:21 +0000 Subject: [PATCH 2/9] Add support for optional enum queryables --- .../stac_fastapi/core/base_database_logic.py | 14 ++++++- .../stac_fastapi/core/extensions/filter.py | 11 ++++++ .../elasticsearch/database_logic.py | 25 +++++++++++++ .../stac_fastapi/opensearch/database_logic.py | 25 +++++++++++++ .../sfeos_helpers/filter/client.py | 37 ++++++++++++------- 5 files changed, 97 insertions(+), 15 deletions(-) diff --git a/stac_fastapi/core/stac_fastapi/core/base_database_logic.py b/stac_fastapi/core/stac_fastapi/core/base_database_logic.py index 0043cfb8..57ca9437 100644 --- a/stac_fastapi/core/stac_fastapi/core/base_database_logic.py +++ b/stac_fastapi/core/stac_fastapi/core/base_database_logic.py @@ -1,7 +1,7 @@ """Base database logic.""" import abc -from typing import Any, Dict, Iterable, Optional +from typing import Any, Dict, Iterable, List, Optional class BaseDatabaseLogic(abc.ABC): @@ -36,6 +36,18 @@ async def delete_item( """Delete an item from the database.""" pass + @abc.abstractmethod + async def get_items_mapping(self, collection_id: str) -> Dict[str, Dict[str, Any]]: + """Get the mapping for the items in the collection.""" + pass + + @abc.abstractmethod + async def get_items_unique_values( + self, collection_id: str, field_names: Iterable[str], *, limit: int = ... + ) -> Dict[str, List[str]]: + """Get the unique values for the given fields in the collection.""" + pass + @abc.abstractmethod async def create_collection(self, collection: Dict, refresh: bool = False) -> None: """Create a collection in the database.""" diff --git a/stac_fastapi/core/stac_fastapi/core/extensions/filter.py b/stac_fastapi/core/stac_fastapi/core/extensions/filter.py index 08200030..c6859672 100644 --- a/stac_fastapi/core/stac_fastapi/core/extensions/filter.py +++ b/stac_fastapi/core/stac_fastapi/core/extensions/filter.py @@ -60,6 +60,17 @@ "maximum": 100, }, } +"""Queryables that are present in all collections.""" + +OPTIONAL_QUERYABLES: Dict[str, Dict[str, Any]] = { + "platform": { + "$enum": True, + "description": "Satellite platform identifier", + }, +} +"""Queryables that are present in some collections.""" + +ALL_QUERYABLES: Dict[str, Dict[str, Any]] = DEFAULT_QUERYABLES | OPTIONAL_QUERYABLES class LogicalOp(str, Enum): diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py index d529ce01..84917c4b 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py @@ -895,6 +895,31 @@ async def get_items_mapping(self, collection_id: str) -> Dict[str, Any]: except ESNotFoundError: raise NotFoundError(f"Mapping for index {index_name} not found") + async def get_items_unique_values( + self, collection_id: str, field_names: Iterable[str], *, limit: int = 100 + ) -> Dict[str, List[str]]: + """Get the unique values for the given fields in the collection.""" + limit_plus_one = limit + 1 + index_name = index_alias_by_collection_id(collection_id) + + query = await self.client.search( + index=index_name, + body={ + "size": 0, + "aggs": { + field: {"terms": {"field": field, "size": limit_plus_one}} + for field in field_names + }, + }, + ) + + result: Dict[str, List[str]] = {} + for field, agg in query["aggregations"].items(): + if len(agg["buckets"]) > limit: + raise ValueError(f"Field {field} has more than {limit} unique values.") + result[field] = [bucket["key"] for bucket in agg["buckets"]] + return result + async def create_collection(self, collection: Collection, **kwargs: Any): """Create a single collection in the database. diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py index f93311f9..5f8609ee 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py @@ -904,6 +904,31 @@ async def get_items_mapping(self, collection_id: str) -> Dict[str, Any]: except exceptions.NotFoundError: raise NotFoundError(f"Mapping for index {index_name} not found") + async def get_items_unique_values( + self, collection_id: str, field_names: Iterable[str], *, limit: int = 100 + ) -> Dict[str, List[str]]: + """Get the unique values for the given fields in the collection.""" + limit_plus_one = limit + 1 + index_name = index_alias_by_collection_id(collection_id) + + query = await self.client.search( + index=index_name, + body={ + "size": 0, + "aggs": { + field: {"terms": {"field": field, "size": limit_plus_one}} + for field in field_names + }, + }, + ) + + result: Dict[str, List[str]] = {} + for field, agg in query["aggregations"].items(): + if len(agg["buckets"]) > limit: + raise ValueError(f"Field {field} has more than {limit} unique values.") + result[field] = [bucket["key"] for bucket in agg["buckets"]] + return result + async def create_collection(self, collection: Collection, **kwargs: Any): """Create a single collection in the database. diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/client.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/client.py index 4b2a1a71..9d0eb69b 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/client.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/client.py @@ -1,12 +1,12 @@ """Filter client implementation for Elasticsearch/OpenSearch.""" from collections import deque -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Tuple import attr from stac_fastapi.core.base_database_logic import BaseDatabaseLogic -from stac_fastapi.core.extensions.filter import DEFAULT_QUERYABLES +from stac_fastapi.core.extensions.filter import ALL_QUERYABLES, DEFAULT_QUERYABLES from stac_fastapi.extensions.core.filter.client import AsyncBaseFiltersClient from stac_fastapi.sfeos_helpers.mappings import ES_MAPPING_TYPE_TO_JSON @@ -59,31 +59,31 @@ async def get_queryables( mapping_data = await self.database.get_items_mapping(collection_id) mapping_properties = next(iter(mapping_data.values()))["mappings"]["properties"] - stack = deque(mapping_properties.items()) + stack: deque[Tuple[str, Dict[str, Any]]] = deque(mapping_properties.items()) + enum_fields: Dict[str, Dict[str, Any]] = {} while stack: - field_name, field_def = stack.popleft() + field_fqn, field_def = stack.popleft() # Iterate over nested fields field_properties = field_def.get("properties") if field_properties: - # Fields in Item Properties should be exposed with their un-prefixed names, - # and not require expressions to prefix them with properties, - # e.g., eo:cloud_cover instead of properties.eo:cloud_cover. - if field_name == "properties": - stack.extend(field_properties.items()) - else: - stack.extend( - (f"{field_name}.{k}", v) for k, v in field_properties.items() - ) + stack.extend( + (f"{field_fqn}.{k}", v) for k, v in field_properties.items() + ) # Skip non-indexed or disabled fields field_type = field_def.get("type") if not field_type or not field_def.get("enabled", True): continue + # Fields in Item Properties should be exposed with their un-prefixed names, + # and not require expressions to prefix them with properties, + # e.g., eo:cloud_cover instead of properties.eo:cloud_cover. + field_name = field_fqn.removeprefix("properties.") + # Generate field properties - field_result = DEFAULT_QUERYABLES.get(field_name, {}) + field_result = ALL_QUERYABLES.get(field_name, {}) properties[field_name] = field_result field_name_human = field_name.replace("_", " ").title() @@ -95,4 +95,13 @@ async def get_queryables( if field_type in {"date", "date_nanos"}: field_result.setdefault("format", "date-time") + if field_result.pop("$enum", False): + enum_fields[field_fqn] = field_result + + if enum_fields: + for field_fqn, unique_values in ( + await self.database.get_items_unique_values(collection_id, enum_fields) + ).items(): + enum_fields[field_fqn]["enum"] = unique_values + return queryables From 65da23a3ae42099bce100f5199b86a882a55705e Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Fri, 6 Jun 2025 12:58:19 +0000 Subject: [PATCH 3/9] Add tests for enum queryables --- stac_fastapi/tests/extensions/test_filter.py | 70 +++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index fb6bc850..78515567 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -3,8 +3,15 @@ import os from os import listdir from os.path import isfile, join +from typing import Callable, Dict import pytest +from httpx import AsyncClient +from stac_pydantic import api + +from stac_fastapi.core.core import TransactionsClient + +from ..conftest import MockRequest THIS_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -40,7 +47,6 @@ async def test_filter_extension_collection_link(app_client, load_test_data): @pytest.mark.asyncio async def test_search_filters_post(app_client, ctx): - filters = [] pwd = f"{THIS_DIR}/cql2" for fn in [fn for f in listdir(pwd) if isfile(fn := join(pwd, f))]: @@ -625,3 +631,65 @@ async def test_search_filter_extension_cql2text_s_disjoint_property(app_client, assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 + + +@pytest.mark.asyncio +async def test_queryables_enum_platform( + app_client: AsyncClient, + txn_client: TransactionsClient, + load_test_data: Callable[[str], Dict], +): + # Arrange + # Create collection + collection_data = load_test_data("test_collection.json") + collection_id = collection_data["id"] = "enum-test-collection" + await txn_client.create_collection( + api.Collection(**collection_data), request=MockRequest + ) + + # Create items with different platform values + item_data_1 = load_test_data("test_item.json") + item_data_1["id"] = "enum-test-item-1" + item_data_1["properties"]["platform"] = "landsat-8" + await txn_client.create_item( + collection_id=collection_id, + item=api.Item(**item_data_1), + request=MockRequest, + ) + + item_data_2 = load_test_data("test_item.json") + item_data_2["id"] = "enum-test-item-2" + item_data_2["properties"]["platform"] = "sentinel-2" + await txn_client.create_item( + collection_id=collection_id, + item=api.Item(**item_data_2), + request=MockRequest, + ) + + item_data_3 = load_test_data("test_item.json") + item_data_3["id"] = "enum-test-item-3" + item_data_3["properties"]["platform"] = "landsat-8" + await txn_client.create_item( + collection_id=collection_id, + item=api.Item(**item_data_3), + request=MockRequest, + refresh=True, + ) + + # Act + # Test queryables endpoint + queryables = ( + (await app_client.get(f"/collections/{collection_data['id']}/queryables")) + .raise_for_status() + .json() + ) + + # Assert + # Verify distinct values (should only have 2 unique values despite 3 items) + properties = queryables["properties"] + platform_info = properties["platform"] + platform_values = platform_info["enum"] + assert set(platform_values) == {"landsat-8", "sentinel-2"} + + # Clean up + await txn_client.delete_collection(collection_id) From 3549806ab0d4cf65b458e70b3fa3a73bdf383d06 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Fri, 6 Jun 2025 13:00:32 +0000 Subject: [PATCH 4/9] Update CHANGELOG to include support for enum queryables --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f056e26..552a28a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Added support for enum queryables [#390](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/390) + ## [v5.0.0a1] - 2025-05-30 ### Changed From ab2eb10e421f8ce2df4d07cc287262b3e826d017 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Fri, 6 Jun 2025 13:05:50 +0000 Subject: [PATCH 5/9] Generate item_data in a loop, Ensure correct "collection" in item_data --- stac_fastapi/tests/extensions/test_filter.py | 39 ++++++-------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index 78515567..32cfdc9c 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -648,33 +648,18 @@ async def test_queryables_enum_platform( ) # Create items with different platform values - item_data_1 = load_test_data("test_item.json") - item_data_1["id"] = "enum-test-item-1" - item_data_1["properties"]["platform"] = "landsat-8" - await txn_client.create_item( - collection_id=collection_id, - item=api.Item(**item_data_1), - request=MockRequest, - ) - - item_data_2 = load_test_data("test_item.json") - item_data_2["id"] = "enum-test-item-2" - item_data_2["properties"]["platform"] = "sentinel-2" - await txn_client.create_item( - collection_id=collection_id, - item=api.Item(**item_data_2), - request=MockRequest, - ) - - item_data_3 = load_test_data("test_item.json") - item_data_3["id"] = "enum-test-item-3" - item_data_3["properties"]["platform"] = "landsat-8" - await txn_client.create_item( - collection_id=collection_id, - item=api.Item(**item_data_3), - request=MockRequest, - refresh=True, - ) + NUM_ITEMS = 3 + for i in range(1, NUM_ITEMS + 1): + item_data = load_test_data("test_item.json") + item_data["id"] = f"enum-test-item-{i}" + item_data["collection"] = collection_id + item_data["properties"]["platform"] = "landsat-8" if i % 2 else "sentinel-2" + await txn_client.create_item( + collection_id=collection_id, + item=api.Item(**item_data), + request=MockRequest, + refresh=i == NUM_ITEMS, + ) # Act # Test queryables endpoint From 92ed789a0ac2ccfbbcaeb4f1cfc170e2097c10ec Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Fri, 6 Jun 2025 13:18:31 +0000 Subject: [PATCH 6/9] Get rid of txn_client in test_queryables_enum_platform --- stac_fastapi/tests/extensions/test_filter.py | 30 +++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index 32cfdc9c..e54d198e 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -1,17 +1,13 @@ import json import logging import os +import uuid from os import listdir from os.path import isfile, join from typing import Callable, Dict import pytest from httpx import AsyncClient -from stac_pydantic import api - -from stac_fastapi.core.core import TransactionsClient - -from ..conftest import MockRequest THIS_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -636,16 +632,19 @@ async def test_search_filter_extension_cql2text_s_disjoint_property(app_client, @pytest.mark.asyncio async def test_queryables_enum_platform( app_client: AsyncClient, - txn_client: TransactionsClient, load_test_data: Callable[[str], Dict], + monkeypatch: pytest.MonkeyPatch, ): # Arrange + # Enforce instant database refresh + # TODO: Is there a better way to do this? + monkeypatch.setenv("DATABASE_REFRESH", "true") + # Create collection collection_data = load_test_data("test_collection.json") - collection_id = collection_data["id"] = "enum-test-collection" - await txn_client.create_collection( - api.Collection(**collection_data), request=MockRequest - ) + collection_id = collection_data["id"] = f"enum-test-collection-{uuid.uuid4()}" + r = await app_client.post("/collections", json=collection_data) + r.raise_for_status() # Create items with different platform values NUM_ITEMS = 3 @@ -654,12 +653,8 @@ async def test_queryables_enum_platform( item_data["id"] = f"enum-test-item-{i}" item_data["collection"] = collection_id item_data["properties"]["platform"] = "landsat-8" if i % 2 else "sentinel-2" - await txn_client.create_item( - collection_id=collection_id, - item=api.Item(**item_data), - request=MockRequest, - refresh=i == NUM_ITEMS, - ) + r = await app_client.post(f"/collections/{collection_id}/items", json=item_data) + r.raise_for_status() # Act # Test queryables endpoint @@ -677,4 +672,5 @@ async def test_queryables_enum_platform( assert set(platform_values) == {"landsat-8", "sentinel-2"} # Clean up - await txn_client.delete_collection(collection_id) + r = await app_client.delete(f"/collections/{collection_id}") + r.raise_for_status() From bf79ba4dcc573062465bd3ad1e963c5133db2429 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Fri, 6 Jun 2025 14:02:38 +0000 Subject: [PATCH 7/9] Synchronize tests-production behavior for FilterExtension See https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/394 --- stac_fastapi/tests/conftest.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index afb9ac9b..33d86a96 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -28,6 +28,7 @@ from stac_fastapi.core.route_dependencies import get_route_dependencies from stac_fastapi.core.utilities import get_bool_env from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient +from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings @@ -39,9 +40,11 @@ ) else: from stac_fastapi.elasticsearch.config import ( - ElasticsearchSettings as SearchSettings, AsyncElasticsearchSettings as AsyncSettings, ) + from stac_fastapi.elasticsearch.config import ( + ElasticsearchSettings as SearchSettings, + ) from stac_fastapi.elasticsearch.database_logic import ( DatabaseLogic, create_collection_index, @@ -198,6 +201,13 @@ def bulk_txn_client(): async def app(): settings = AsyncSettings() + filter_extension = FilterExtension( + client=EsAsyncBaseFiltersClient(database=database) + ) + filter_extension.conformance_classes.append( + "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators" + ) + aggregation_extension = AggregationExtension( client=EsAsyncBaseAggregationClient( database=database, session=None, settings=settings @@ -217,7 +227,7 @@ async def app(): FieldsExtension(), QueryExtension(), TokenPaginationExtension(), - FilterExtension(), + filter_extension, FreeTextExtension(), ] @@ -313,7 +323,6 @@ async def app_client_rate_limit(app_rate_limit): @pytest_asyncio.fixture(scope="session") async def app_basic_auth(): - stac_fastapi_route_dependencies = """[ { "routes":[{"method":"*","path":"*"}], From 636e98877788652b342009b2fe01a3c13aa24a27 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Sun, 8 Jun 2025 07:27:54 +0200 Subject: [PATCH 8/9] Use FilterConformanceClasses instead of hard-coded strings for ADVANCED_COMPARISON_OPERATORS --- stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py | 3 ++- stac_fastapi/opensearch/stac_fastapi/opensearch/app.py | 3 ++- stac_fastapi/tests/conftest.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 3f22d7ab..cda5a464 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -37,6 +37,7 @@ TokenPaginationExtension, TransactionExtension, ) +from stac_fastapi.extensions.core.filter import FilterConformanceClasses from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient @@ -56,7 +57,7 @@ client=EsAsyncBaseFiltersClient(database=database_logic) ) filter_extension.conformance_classes.append( - "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators" + FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS ) aggregation_extension = AggregationExtension( diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index 0d11369e..66671ff0 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -31,6 +31,7 @@ TokenPaginationExtension, TransactionExtension, ) +from stac_fastapi.extensions.core.filter import FilterConformanceClasses from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_fastapi.opensearch.config import OpensearchSettings from stac_fastapi.opensearch.database_logic import ( @@ -56,7 +57,7 @@ client=EsAsyncBaseFiltersClient(database=database_logic) ) filter_extension.conformance_classes.append( - "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators" + FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS ) aggregation_extension = AggregationExtension( diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 33d86a96..7d8c1113 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -27,6 +27,7 @@ from stac_fastapi.core.rate_limit import setup_rate_limit from stac_fastapi.core.route_dependencies import get_route_dependencies from stac_fastapi.core.utilities import get_bool_env +from stac_fastapi.extensions.core.filter import FilterConformanceClasses from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient @@ -205,7 +206,7 @@ async def app(): client=EsAsyncBaseFiltersClient(database=database) ) filter_extension.conformance_classes.append( - "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators" + FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS ) aggregation_extension = AggregationExtension( From 6900f27c1035ca620035a29444c2236a84986332 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Sun, 8 Jun 2025 07:38:59 +0200 Subject: [PATCH 9/9] Use logger.warning instead of ValueError for too many unique values in enum --- .../stac_fastapi/elasticsearch/database_logic.py | 8 +++++++- .../opensearch/stac_fastapi/opensearch/database_logic.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py index 84917c4b..a1ca6250 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py @@ -916,7 +916,13 @@ async def get_items_unique_values( result: Dict[str, List[str]] = {} for field, agg in query["aggregations"].items(): if len(agg["buckets"]) > limit: - raise ValueError(f"Field {field} has more than {limit} unique values.") + logger.warning( + "Skipping enum field %s: exceeds limit of %d unique values. " + "Consider excluding this field from enumeration or increase the limit.", + field, + limit, + ) + continue result[field] = [bucket["key"] for bucket in agg["buckets"]] return result diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py index 5f8609ee..88c7fcdc 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py @@ -925,7 +925,13 @@ async def get_items_unique_values( result: Dict[str, List[str]] = {} for field, agg in query["aggregations"].items(): if len(agg["buckets"]) > limit: - raise ValueError(f"Field {field} has more than {limit} unique values.") + logger.warning( + "Skipping enum field %s: exceeds limit of %d unique values. " + "Consider excluding this field from enumeration or increase the limit.", + field, + limit, + ) + continue result[field] = [bucket["key"] for bucket in agg["buckets"]] return result