Skip to content

Commit bc2930c

Browse files
committed
Add support for optional enum queryables
1 parent 824357c commit bc2930c

File tree

5 files changed

+97
-15
lines changed

5 files changed

+97
-15
lines changed

stac_fastapi/core/stac_fastapi/core/base_database_logic.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Base database logic."""
22

33
import abc
4-
from typing import Any, Dict, Iterable, Optional
4+
from typing import Any, Dict, Iterable, List, Optional
55

66

77
class BaseDatabaseLogic(abc.ABC):
@@ -36,6 +36,18 @@ async def delete_item(
3636
"""Delete an item from the database."""
3737
pass
3838

39+
@abc.abstractmethod
40+
async def get_items_mapping(self, collection_id: str) -> Dict[str, Dict[str, Any]]:
41+
"""Get the mapping for the items in the collection."""
42+
pass
43+
44+
@abc.abstractmethod
45+
async def get_items_unique_values(
46+
self, collection_id: str, field_names: Iterable[str], *, limit: int = ...
47+
) -> Dict[str, List[str]]:
48+
"""Get the unique values for the given fields in the collection."""
49+
pass
50+
3951
@abc.abstractmethod
4052
async def create_collection(self, collection: Dict, refresh: bool = False) -> None:
4153
"""Create a collection in the database."""

stac_fastapi/core/stac_fastapi/core/extensions/filter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@
6060
"maximum": 100,
6161
},
6262
}
63+
"""Queryables that are present in all collections."""
64+
65+
OPTIONAL_QUERYABLES: Dict[str, Dict[str, Any]] = {
66+
"platform": {
67+
"$enum": True,
68+
"description": "Satellite platform identifier",
69+
},
70+
}
71+
"""Queryables that are present in some collections."""
72+
73+
ALL_QUERYABLES: Dict[str, Dict[str, Any]] = DEFAULT_QUERYABLES | OPTIONAL_QUERYABLES
6374

6475

6576
class LogicalOp(str, Enum):

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,31 @@ async def get_items_mapping(self, collection_id: str) -> Dict[str, Any]:
895895
except ESNotFoundError:
896896
raise NotFoundError(f"Mapping for index {index_name} not found")
897897

898+
async def get_items_unique_values(
899+
self, collection_id: str, field_names: Iterable[str], *, limit: int = 100
900+
) -> Dict[str, List[str]]:
901+
"""Get the unique values for the given fields in the collection."""
902+
limit_plus_one = limit + 1
903+
index_name = index_alias_by_collection_id(collection_id)
904+
905+
query = await self.client.search(
906+
index=index_name,
907+
body={
908+
"size": 0,
909+
"aggs": {
910+
field: {"terms": {"field": field, "size": limit_plus_one}}
911+
for field in field_names
912+
},
913+
},
914+
)
915+
916+
result: Dict[str, List[str]] = {}
917+
for field, agg in query["aggregations"].items():
918+
if len(agg["buckets"]) > limit:
919+
raise ValueError(f"Field {field} has more than {limit} unique values.")
920+
result[field] = [bucket["key"] for bucket in agg["buckets"]]
921+
return result
922+
898923
async def create_collection(self, collection: Collection, **kwargs: Any):
899924
"""Create a single collection in the database.
900925

stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,31 @@ async def get_items_mapping(self, collection_id: str) -> Dict[str, Any]:
904904
except exceptions.NotFoundError:
905905
raise NotFoundError(f"Mapping for index {index_name} not found")
906906

907+
async def get_items_unique_values(
908+
self, collection_id: str, field_names: Iterable[str], *, limit: int = 100
909+
) -> Dict[str, List[str]]:
910+
"""Get the unique values for the given fields in the collection."""
911+
limit_plus_one = limit + 1
912+
index_name = index_alias_by_collection_id(collection_id)
913+
914+
query = await self.client.search(
915+
index=index_name,
916+
body={
917+
"size": 0,
918+
"aggs": {
919+
field: {"terms": {"field": field, "size": limit_plus_one}}
920+
for field in field_names
921+
},
922+
},
923+
)
924+
925+
result: Dict[str, List[str]] = {}
926+
for field, agg in query["aggregations"].items():
927+
if len(agg["buckets"]) > limit:
928+
raise ValueError(f"Field {field} has more than {limit} unique values.")
929+
result[field] = [bucket["key"] for bucket in agg["buckets"]]
930+
return result
931+
907932
async def create_collection(self, collection: Collection, **kwargs: Any):
908933
"""Create a single collection in the database.
909934

stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/client.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Filter client implementation for Elasticsearch/OpenSearch."""
22

33
from collections import deque
4-
from typing import Any, Dict, Optional
4+
from typing import Any, Dict, Optional, Tuple
55

66
import attr
77

88
from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
9-
from stac_fastapi.core.extensions.filter import DEFAULT_QUERYABLES
9+
from stac_fastapi.core.extensions.filter import ALL_QUERYABLES, DEFAULT_QUERYABLES
1010
from stac_fastapi.extensions.core.filter.client import AsyncBaseFiltersClient
1111
from stac_fastapi.sfeos_helpers.mappings import ES_MAPPING_TYPE_TO_JSON
1212

@@ -59,31 +59,31 @@ async def get_queryables(
5959

6060
mapping_data = await self.database.get_items_mapping(collection_id)
6161
mapping_properties = next(iter(mapping_data.values()))["mappings"]["properties"]
62-
stack = deque(mapping_properties.items())
62+
stack: deque[Tuple[str, Dict[str, Any]]] = deque(mapping_properties.items())
63+
enum_fields: Dict[str, Dict[str, Any]] = {}
6364

6465
while stack:
65-
field_name, field_def = stack.popleft()
66+
field_fqn, field_def = stack.popleft()
6667

6768
# Iterate over nested fields
6869
field_properties = field_def.get("properties")
6970
if field_properties:
70-
# Fields in Item Properties should be exposed with their un-prefixed names,
71-
# and not require expressions to prefix them with properties,
72-
# e.g., eo:cloud_cover instead of properties.eo:cloud_cover.
73-
if field_name == "properties":
74-
stack.extend(field_properties.items())
75-
else:
76-
stack.extend(
77-
(f"{field_name}.{k}", v) for k, v in field_properties.items()
78-
)
71+
stack.extend(
72+
(f"{field_fqn}.{k}", v) for k, v in field_properties.items()
73+
)
7974

8075
# Skip non-indexed or disabled fields
8176
field_type = field_def.get("type")
8277
if not field_type or not field_def.get("enabled", True):
8378
continue
8479

80+
# Fields in Item Properties should be exposed with their un-prefixed names,
81+
# and not require expressions to prefix them with properties,
82+
# e.g., eo:cloud_cover instead of properties.eo:cloud_cover.
83+
field_name = field_fqn.removeprefix("properties.")
84+
8585
# Generate field properties
86-
field_result = DEFAULT_QUERYABLES.get(field_name, {})
86+
field_result = ALL_QUERYABLES.get(field_name, {})
8787
properties[field_name] = field_result
8888

8989
field_name_human = field_name.replace("_", " ").title()
@@ -95,4 +95,13 @@ async def get_queryables(
9595
if field_type in {"date", "date_nanos"}:
9696
field_result.setdefault("format", "date-time")
9797

98+
if field_result.pop("$enum", False):
99+
enum_fields[field_fqn] = field_result
100+
101+
if enum_fields:
102+
for field_fqn, unique_values in (
103+
await self.database.get_items_unique_values(collection_id, enum_fields)
104+
).items():
105+
enum_fields[field_fqn]["enum"] = unique_values
106+
98107
return queryables

0 commit comments

Comments
 (0)