Skip to content

Commit e7943ba

Browse files
committed
Dynamic queryables mapping for properties.
1 parent c1d9ca8 commit e7943ba

File tree

2 files changed

+58
-25
lines changed

2 files changed

+58
-25
lines changed

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

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,20 @@ class SpatialOp(str, Enum):
9191
S_DISJOINT = "s_disjoint"
9292

9393

94-
queryables_mapping = {
95-
"id": "id",
96-
"collection": "collection",
97-
"geometry": "geometry",
98-
"datetime": "properties.datetime",
99-
"created": "properties.created",
100-
"updated": "properties.updated",
101-
"cloud_cover": "properties.eo:cloud_cover",
102-
"cloud_shadow_percentage": "properties.s2:cloud_shadow_percentage",
103-
"nodata_pixel_percentage": "properties.s2:nodata_pixel_percentage",
104-
}
105-
106-
107-
def to_es_field(field: str) -> str:
94+
# queryables_mapping = {
95+
# "id": "id",
96+
# "collection": "collection",
97+
# "geometry": "geometry",
98+
# "datetime": "properties.datetime",
99+
# "created": "properties.created",
100+
# "updated": "properties.updated",
101+
# "cloud_cover": "properties.eo:cloud_cover",
102+
# "cloud_shadow_percentage": "properties.s2:cloud_shadow_percentage",
103+
# "nodata_pixel_percentage": "properties.s2:nodata_pixel_percentage",
104+
# }
105+
106+
107+
def to_es_field(queryables_mapping: Dict[str, Any], field: str) -> str:
108108
"""
109109
Map a given field to its corresponding Elasticsearch field according to a predefined mapping.
110110
@@ -117,7 +117,7 @@ def to_es_field(field: str) -> str:
117117
return queryables_mapping.get(field, field)
118118

119119

120-
def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
120+
def to_es(queryables_mapping: Dict[str, Any], query: Dict[str, Any]) -> Dict[str, Any]:
121121
"""
122122
Transform a simplified CQL2 query structure to an Elasticsearch compatible query DSL.
123123
@@ -133,7 +133,13 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
133133
LogicalOp.OR: "should",
134134
LogicalOp.NOT: "must_not",
135135
}[query["op"]]
136-
return {"bool": {bool_type: [to_es(sub_query) for sub_query in query["args"]]}}
136+
return {
137+
"bool": {
138+
bool_type: [
139+
to_es(queryables_mapping, sub_query) for sub_query in query["args"]
140+
]
141+
}
142+
}
137143

138144
elif query["op"] in [
139145
ComparisonOp.EQ,
@@ -150,7 +156,7 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
150156
ComparisonOp.GTE: "gte",
151157
}
152158

153-
field = to_es_field(query["args"][0]["property"])
159+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
154160
value = query["args"][1]
155161
if isinstance(value, dict) and "timestamp" in value:
156162
value = value["timestamp"]
@@ -173,11 +179,11 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
173179
return {"range": {field: {range_op[query["op"]]: value}}}
174180

175181
elif query["op"] == ComparisonOp.IS_NULL:
176-
field = to_es_field(query["args"][0]["property"])
182+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
177183
return {"bool": {"must_not": {"exists": {"field": field}}}}
178184

179185
elif query["op"] == AdvancedComparisonOp.BETWEEN:
180-
field = to_es_field(query["args"][0]["property"])
186+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
181187
gte, lte = query["args"][1], query["args"][2]
182188
if isinstance(gte, dict) and "timestamp" in gte:
183189
gte = gte["timestamp"]
@@ -186,14 +192,14 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
186192
return {"range": {field: {"gte": gte, "lte": lte}}}
187193

188194
elif query["op"] == AdvancedComparisonOp.IN:
189-
field = to_es_field(query["args"][0]["property"])
195+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
190196
values = query["args"][1]
191197
if not isinstance(values, list):
192198
raise ValueError(f"Arg {values} is not a list")
193199
return {"terms": {field: values}}
194200

195201
elif query["op"] == AdvancedComparisonOp.LIKE:
196-
field = to_es_field(query["args"][0]["property"])
202+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
197203
pattern = cql2_like_to_es(query["args"][1])
198204
return {"wildcard": {field: {"value": pattern, "case_insensitive": True}}}
199205

@@ -203,7 +209,7 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
203209
SpatialOp.S_WITHIN,
204210
SpatialOp.S_DISJOINT,
205211
]:
206-
field = to_es_field(query["args"][0]["property"])
212+
field = to_es_field(queryables_mapping, query["args"][0]["property"])
207213
geometry = query["args"][1]
208214

209215
relation_mapping = {

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,34 @@ async def get_one_item(self, collection_id: str, item_id: str) -> Dict:
290290
)
291291
return item["_source"]
292292

293+
async def get_queryables_mapping(self, collection_id: str = "*") -> dict:
294+
"""Retrieve mapping of Queryables for search.
295+
296+
Args:
297+
collection_id (str, optional): The id of the Collection the Queryables
298+
belongs to. Defaults to "*".
299+
300+
Returns:
301+
dict: A dictionary containing the Queryables mappings.
302+
"""
303+
queryables_mapping = {}
304+
305+
mappings = await self.client.indices.get_mapping(
306+
index=f"{ITEMS_INDEX_PREFIX}{collection_id}",
307+
)
308+
309+
for mapping in mappings.values():
310+
fields = mapping["mappings"]["properties"]
311+
properties = fields.pop("properties")
312+
313+
for field_key in fields:
314+
queryables_mapping[field_key] = field_key
315+
316+
for property_key in properties["properties"]:
317+
queryables_mapping[property_key] = f"properties.{property_key}"
318+
319+
return queryables_mapping
320+
293321
@staticmethod
294322
def make_search():
295323
"""Database logic to create a Search instance."""
@@ -518,8 +546,7 @@ def apply_free_text_filter(search: Search, free_text_queries: Optional[List[str]
518546

519547
return search
520548

521-
@staticmethod
522-
def apply_cql2_filter(search: Search, _filter: Optional[Dict[str, Any]]):
549+
def apply_cql2_filter(self, search: Search, _filter: Optional[Dict[str, Any]]):
523550
"""
524551
Apply a CQL2 filter to an Elasticsearch Search object.
525552
@@ -539,7 +566,7 @@ def apply_cql2_filter(search: Search, _filter: Optional[Dict[str, Any]]):
539566
otherwise the original Search object.
540567
"""
541568
if _filter is not None:
542-
es_query = filter.to_es(_filter)
569+
es_query = filter.to_es(self.get_queryables_mapping(), _filter)
543570
search = search.query(es_query)
544571

545572
return search

0 commit comments

Comments
 (0)