Skip to content

Commit d74e386

Browse files
author
Phil Varner
committed
convert all uses of query to filter
1 parent fefdfb2 commit d74e386

File tree

3 files changed

+94
-105
lines changed

3 files changed

+94
-105
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ For changes, see the [Changelog](CHANGELOG.md).
1111
To install the classes in your local Python env, run:
1212

1313
```shell
14-
cd stac_fastapi/elasticsearch
15-
pip install -e '.[dev]'
14+
pip install -e 'stac_fastapi/elasticsearch[dev]'
1615
```
1716

1817
### Pre-commit

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,18 @@ async def item_collection(
9191
links = []
9292
base_url = str(kwargs["request"].base_url)
9393

94-
serialized_children, count = await self.database.get_item_collection(
94+
serialized_children, maybe_count = await self.database.get_collection_items(
9595
collection_id=collection_id, limit=limit, base_url=base_url
9696
)
9797

9898
context_obj = None
9999
if self.extension_is_enabled("ContextExtension"):
100100
context_obj = {
101-
"returned": count if count is not None and count < limit else limit,
101+
"returned": len(serialized_children),
102102
"limit": limit,
103-
"matched": count,
104103
}
104+
if maybe_count is not None:
105+
context_obj["matched"] = maybe_count
105106

106107
return ItemCollection(
107108
type="FeatureCollection",
@@ -207,29 +208,29 @@ async def post_search(
207208
) -> ItemCollection:
208209
"""POST search catalog."""
209210
base_url = str(kwargs["request"].base_url)
210-
search = self.database.create_search()
211+
search = self.database.make_search()
211212

212213
if search_request.query:
213214
for (field_name, expr) in search_request.query.items():
214215
field = "properties__" + field_name
215216
for (op, value) in expr.items():
216-
search = self.database.create_query_filter(
217+
search = self.database.apply_stacql_filter(
217218
search=search, op=op, field=field, value=value
218219
)
219220

220221
if search_request.ids:
221-
search = self.database.search_ids(
222+
search = self.database.apply_ids_filter(
222223
search=search, item_ids=search_request.ids
223224
)
224225

225226
if search_request.collections:
226-
search = self.database.filter_collections(
227+
search = self.database.apply_collections_filter(
227228
search=search, collection_ids=search_request.collections
228229
)
229230

230231
if search_request.datetime:
231232
datetime_search = self._return_date(search_request.datetime)
232-
search = self.database.search_datetime(
233+
search = self.database.apply_datetime_filter(
233234
search=search, datetime_search=datetime_search
234235
)
235236

@@ -238,22 +239,22 @@ async def post_search(
238239
if len(bbox) == 6:
239240
bbox = [bbox[0], bbox[1], bbox[3], bbox[4]]
240241

241-
search = self.database.search_bbox(search=search, bbox=bbox)
242+
search = self.database.apply_bbox_filter(search=search, bbox=bbox)
242243

243244
if search_request.intersects:
244-
self.database.search_intersects(
245+
self.database.apply_intersects_filter(
245246
search=search, intersects=search_request.intersects
246247
)
247248

248249
if search_request.sortby:
249250
for sort in search_request.sortby:
250251
if sort.field == "datetime":
251252
sort.field = "properties__datetime"
252-
search = self.database.sort_field(
253+
search = self.database.apply_sort(
253254
search=search, field=sort.field, direction=sort.direction
254255
)
255256

256-
count = await self.database.search_count(search=search)
257+
maybe_count = await self.database.search_count(search=search)
257258

258259
response_features = await self.database.execute_search(
259260
search=search, limit=search_request.limit, base_url=base_url
@@ -289,16 +290,16 @@ async def post_search(
289290
context_obj = None
290291
if self.extension_is_enabled("ContextExtension"):
291292
context_obj = {
292-
"returned": count if count < limit else limit,
293+
"returned": len(response_features),
293294
"limit": limit,
294-
"matched": count,
295295
}
296+
if maybe_count is not None:
297+
context_obj["matched"] = maybe_count
296298

297-
links = []
298299
return ItemCollection(
299300
type="FeatureCollection",
300301
features=response_features,
301-
links=links,
302+
links=[],
302303
context=context_obj,
303304
)
304305

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 76 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
ITEMS_INDEX = "stac_items"
3232
COLLECTIONS_INDEX = "stac_collections"
3333

34+
DEFAULT_SORT = {
35+
"properties.datetime": {"order": "desc"},
36+
"id": {"order": "desc"},
37+
"collection": {"order": "desc"},
38+
}
39+
40+
41+
def bbox2polygon(b0, b1, b2, b3):
42+
"""Transform bbox to polygon."""
43+
return [[[b0, b1], [b2, b1], [b2, b3], [b0, b3], [b0, b1]]]
44+
3445

3546
def mk_item_id(item_id: str, collection_id: str):
3647
"""Make the Elasticsearch document _id value from the Item id and collection."""
@@ -43,6 +54,7 @@ class DatabaseLogic:
4354

4455
client = AsyncElasticsearchSettings().create_client
4556
sync_client = SyncElasticsearchSettings().create_client
57+
4658
item_serializer: Type[serializers.ItemSerializer] = attr.ib(
4759
default=serializers.ItemSerializer
4860
)
@@ -54,43 +66,30 @@ class DatabaseLogic:
5466

5567
async def get_all_collections(self, base_url: str) -> List[Collection]:
5668
"""Database logic to retrieve a list of all collections."""
57-
collections = await self.client.search(
58-
index=COLLECTIONS_INDEX, query={"match_all": {}}
59-
)
69+
# https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/65
70+
# collections should be paginated, but at least return more than the default 10 for now
71+
collections = await self.client.search(index=COLLECTIONS_INDEX, size=1000)
6072

6173
return [
6274
self.collection_serializer.db_to_stac(c["_source"], base_url=base_url)
6375
for c in collections["hits"]["hits"]
6476
]
6577

66-
async def search_count(self, search: Search) -> int:
78+
async def search_count(self, search: Search) -> Optional[int]:
6779
"""Database logic to count search results."""
6880
return (
6981
await self.client.count(index=ITEMS_INDEX, body=search.to_dict(count=True))
7082
).get("count")
7183

72-
async def get_item_collection(
84+
async def get_collection_items(
7385
self, collection_id: str, limit: int, base_url: str
7486
) -> Tuple[List[Item], Optional[int]]:
7587
"""Database logic to retrieve an ItemCollection and a count of items contained."""
76-
search = self.create_search()
77-
search = search.filter("term", collection=collection_id)
78-
79-
count = await self.search_count(search)
80-
81-
search = search.query()[0:limit]
82-
83-
body = search.to_dict()
84-
es_response = await self.client.search(
85-
index=ITEMS_INDEX, query=body["query"], sort=body.get("sort")
86-
)
87-
88-
serialized_children = [
89-
self.item_serializer.db_to_stac(item["_source"], base_url=base_url)
90-
for item in es_response["hits"]["hits"]
91-
]
88+
search = self.apply_collections_filter(Search(), [collection_id])
89+
items = await self.execute_search(search=search, limit=limit, base_url=base_url)
90+
maybe_count = await self.search_count(search)
9291

93-
return serialized_children, count
92+
return items, maybe_count
9493

9594
async def get_one_item(self, collection_id: str, item_id: str) -> Dict:
9695
"""Database logic to retrieve a single item."""
@@ -105,48 +104,26 @@ async def get_one_item(self, collection_id: str, item_id: str) -> Dict:
105104
return item["_source"]
106105

107106
@staticmethod
108-
def create_search():
109-
"""Database logic to create a nosql Search instance."""
110-
return Search().sort(
111-
{"properties.datetime": {"order": "desc"}},
112-
{"id": {"order": "desc"}},
113-
{"collection": {"order": "desc"}},
114-
)
115-
116-
@staticmethod
117-
def create_query_filter(search: Search, op: str, field: str, value: float):
118-
"""Database logic to perform query for search endpoint."""
119-
if op != "eq":
120-
key_filter = {field: {f"{op}": value}}
121-
search = search.query(Q("range", **key_filter))
122-
else:
123-
search = search.query("match_phrase", **{field: value})
124-
125-
return search
107+
def make_search():
108+
"""Database logic to create a Search instance."""
109+
return Search().sort(*DEFAULT_SORT)
126110

127111
@staticmethod
128-
def search_ids(search: Search, item_ids: List[str]):
112+
def apply_ids_filter(search: Search, item_ids: List[str]):
129113
"""Database logic to search a list of STAC item ids."""
130-
return search.query(
131-
Q("bool", should=[Q("term", **{"id": i_id}) for i_id in item_ids])
132-
)
114+
return search.filter("terms", id=item_ids)
133115

134116
@staticmethod
135-
def filter_collections(search: Search, collection_ids: List):
117+
def apply_collections_filter(search: Search, collection_ids: List[str]):
136118
"""Database logic to search a list of STAC collection ids."""
137-
return search.query(
138-
Q(
139-
"bool",
140-
should=[Q("term", **{"collection": c_id}) for c_id in collection_ids],
141-
)
142-
)
119+
return search.filter("terms", collection=collection_ids)
143120

144121
@staticmethod
145-
def search_datetime(search: Search, datetime_search):
122+
def apply_datetime_filter(search: Search, datetime_search):
146123
"""Database logic to search datetime field."""
147124
if "eq" in datetime_search:
148-
search = search.query(
149-
"match_phrase", **{"properties__datetime": datetime_search["eq"]}
125+
search = search.filter(
126+
"term", **{"properties__datetime": datetime_search["eq"]}
150127
)
151128
else:
152129
search = search.filter(
@@ -158,24 +135,28 @@ def search_datetime(search: Search, datetime_search):
158135
return search
159136

160137
@staticmethod
161-
def search_bbox(search: Search, bbox: List):
138+
def apply_bbox_filter(search: Search, bbox: List):
162139
"""Database logic to search on bounding box."""
163-
polygon = DatabaseLogic.bbox2polygon(bbox[0], bbox[1], bbox[2], bbox[3])
164-
bbox_filter = Q(
165-
{
166-
"geo_shape": {
167-
"geometry": {
168-
"shape": {"type": "polygon", "coordinates": polygon},
169-
"relation": "intersects",
140+
return search.filter(
141+
Q(
142+
{
143+
"geo_shape": {
144+
"geometry": {
145+
"shape": {
146+
"type": "polygon",
147+
"coordinates": bbox2polygon(
148+
bbox[0], bbox[1], bbox[2], bbox[3]
149+
),
150+
},
151+
"relation": "intersects",
152+
}
170153
}
171154
}
172-
}
155+
)
173156
)
174-
search = search.query(bbox_filter)
175-
return search
176157

177158
@staticmethod
178-
def search_intersects(
159+
def apply_intersects_filter(
179160
search: Search,
180161
intersects: Union[
181162
Point,
@@ -188,33 +169,45 @@ def search_intersects(
188169
],
189170
):
190171
"""Database logic to search a geojson object."""
191-
intersect_filter = Q(
192-
{
193-
"geo_shape": {
194-
"geometry": {
195-
"shape": {
196-
"type": intersects.type.lower(),
197-
"coordinates": intersects.coordinates,
198-
},
199-
"relation": "intersects",
172+
return search.filter(
173+
Q(
174+
{
175+
"geo_shape": {
176+
"geometry": {
177+
"shape": {
178+
"type": intersects.type.lower(),
179+
"coordinates": intersects.coordinates,
180+
},
181+
"relation": "intersects",
182+
}
200183
}
201184
}
202-
}
185+
)
203186
)
204-
search = search.query(intersect_filter)
187+
188+
@staticmethod
189+
def apply_stacql_filter(search: Search, op: str, field: str, value: float):
190+
"""Database logic to perform query for search endpoint."""
191+
if op != "eq":
192+
key_filter = {field: {f"{op}": value}}
193+
search = search.filter(Q("range", **key_filter))
194+
else:
195+
search = search.filter("term", **{field: value})
196+
205197
return search
206198

207199
@staticmethod
208-
def sort_field(search: Search, field, direction):
200+
def apply_sort(search: Search, field, direction):
209201
"""Database logic to sort search instance."""
210202
return search.sort({field: {"order": direction}})
211203

212-
async def execute_search(self, search, limit: int, base_url: str) -> List:
204+
async def execute_search(self, search, limit: int, base_url: str) -> List[Item]:
213205
"""Database logic to execute search with limit."""
214-
search = search.query()[0:limit]
206+
search = search[0:limit]
215207
body = search.to_dict()
208+
216209
es_response = await self.client.search(
217-
index=ITEMS_INDEX, query=body["query"], sort=body.get("sort")
210+
index=ITEMS_INDEX, query=body.get("query"), sort=body.get("sort")
218211
)
219212

220213
return [
@@ -330,7 +323,8 @@ def bulk_sync(self, processed_items, refresh: bool = False):
330323
self.sync_client, self._mk_actions(processed_items), refresh=refresh
331324
)
332325

333-
def _mk_actions(self, processed_items):
326+
@staticmethod
327+
def _mk_actions(processed_items):
334328
return [
335329
{
336330
"_index": ITEMS_INDEX,
@@ -357,8 +351,3 @@ async def delete_collections(self) -> None:
357351
body={"query": {"match_all": {}}},
358352
wait_for_completion=True,
359353
)
360-
361-
@staticmethod
362-
def bbox2polygon(b0, b1, b2, b3):
363-
"""Transform bbox to polygon."""
364-
return [[[b0, b1], [b2, b1], [b2, b3], [b0, b3], [b0, b1]]]

0 commit comments

Comments
 (0)