Skip to content

Commit ad4032c

Browse files
committed
Merge branch 'main' into collection-pagination
2 parents edcff23 + e10ee6b commit ad4032c

File tree

6 files changed

+179
-44
lines changed

6 files changed

+179
-44
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
- Collection-level Assets to the CollectionSerializer [#148](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/148)
1313
- Pagination for /collections - GET all collections - route [#164](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/164)
14-
15-
### Added
16-
1714
- Examples folder with example docker setup for running sfes from pip [#147](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/147)
15+
- GET /search filter extension queries [#163](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/163)
16+
- Added support for GET /search intersection queries [#158](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/158)
1817

1918
### Changed
2019

2120
- Update elasticsearch version from 8.1.3 to 8.10.4 in cicd, gh actions [#164](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/164)
21+
- Updated core stac-fastapi libraries to 2.4.8 from 2.4.3 [#151](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/151)
22+
- Use aliases on Elasticsearch indices, add number suffix in index name. [#152](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/152)
2223

2324
### Fixed
2425

stac_fastapi/elasticsearch/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"elasticsearch-dsl==7.4.1",
1818
"pystac[validation]",
1919
"uvicorn",
20+
"orjson",
2021
"overrides",
2122
"starlette",
2223
"geojson-pydantic",

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
"""Item crud client."""
2-
import json
32
import logging
43
from base64 import urlsafe_b64encode
4+
import re
55
from datetime import datetime as datetime_type
66
from datetime import timezone
77
from typing import Any, Dict, List, Optional, Set, Type, Union
8-
from urllib.parse import urljoin
8+
from urllib.parse import unquote_plus, urljoin
99

1010
import attr
11+
import orjson
1112
import stac_pydantic
12-
from fastapi import HTTPException
13+
from fastapi import HTTPException, Request
1314
from overrides import overrides
1415
from pydantic import ValidationError
16+
from pygeofilter.backends.cql2_json import to_cql2
17+
from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
1518
from stac_pydantic.links import Relations
1619
from stac_pydantic.shared import MimeTypes
17-
from starlette.requests import Request
1820

1921
from stac_fastapi.elasticsearch import serializers
2022
from stac_fastapi.elasticsearch.config import ElasticsearchSettings
@@ -303,9 +305,9 @@ def _return_date(interval_str):
303305

304306
return {"lte": end_date, "gte": start_date}
305307

306-
@overrides
307308
async def get_search(
308309
self,
310+
request: Request,
309311
collections: Optional[List[str]] = None,
310312
ids: Optional[List[str]] = None,
311313
bbox: Optional[List[NumType]] = None,
@@ -316,8 +318,8 @@ async def get_search(
316318
fields: Optional[List[str]] = None,
317319
sortby: Optional[str] = None,
318320
intersects: Optional[str] = None,
319-
# filter: Optional[str] = None, # todo: requires fastapi > 2.3 unreleased
320-
# filter_lang: Optional[str] = None, # todo: requires fastapi > 2.3 unreleased
321+
filter: Optional[str] = None,
322+
filter_lang: Optional[str] = None,
321323
**kwargs,
322324
) -> ItemCollection:
323325
"""Get search results from the database.
@@ -347,17 +349,24 @@ async def get_search(
347349
"bbox": bbox,
348350
"limit": limit,
349351
"token": token,
350-
"query": json.loads(query) if query else query,
352+
"query": orjson.loads(query) if query else query,
351353
}
352354

355+
# this is borrowed from stac-fastapi-pgstac
356+
# Kludgy fix because using factory does not allow alias for filter-lan
357+
query_params = str(request.query_params)
358+
if filter_lang is None:
359+
match = re.search(r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE)
360+
if match:
361+
filter_lang = match.group(1)
362+
353363
if datetime:
354364
base_args["datetime"] = datetime
355365

356366
if intersects:
357-
base_args["intersects"] = intersects
367+
base_args["intersects"] = orjson.loads(unquote_plus(intersects))
358368

359369
if sortby:
360-
# https://github.com/radiantearth/stac-spec/tree/master/api-spec/extensions/sort#http-get-or-post-form
361370
sort_param = []
362371
for sort in sortby:
363372
sort_param.append(
@@ -368,12 +377,13 @@ async def get_search(
368377
)
369378
base_args["sortby"] = sort_param
370379

371-
# todo: requires fastapi > 2.3 unreleased
372-
# if filter:
373-
# if filter_lang == "cql2-text":
374-
# base_args["filter-lang"] = "cql2-json"
375-
# base_args["filter"] = orjson.loads(to_cql2(parse_cql2_text(filter)))
376-
# print(f'>>> {base_args["filter"]}')
380+
if filter:
381+
if filter_lang == "cql2-text":
382+
base_args["filter-lang"] = "cql2-json"
383+
base_args["filter"] = orjson.loads(to_cql2(parse_cql2_text(filter)))
384+
else:
385+
base_args["filter-lang"] = "cql2-json"
386+
base_args["filter"] = orjson.loads(unquote_plus(filter))
377387

378388
if fields:
379389
includes = set()
@@ -392,13 +402,12 @@ async def get_search(
392402
search_request = self.post_request_model(**base_args)
393403
except ValidationError:
394404
raise HTTPException(status_code=400, detail="Invalid parameters provided")
395-
resp = await self.post_search(search_request, request=kwargs["request"])
405+
resp = await self.post_search(search_request=search_request, request=request)
396406

397407
return resp
398408

399-
@overrides
400409
async def post_search(
401-
self, search_request: BaseSearchPostRequest, **kwargs
410+
self, search_request: BaseSearchPostRequest, request: Request
402411
) -> ItemCollection:
403412
"""
404413
Perform a POST search on the catalog.
@@ -413,7 +422,6 @@ async def post_search(
413422
Raises:
414423
HTTPException: If there is an error with the cql2_json filter.
415424
"""
416-
request: Request = kwargs["request"]
417425
base_url = str(request.base_url)
418426

419427
search = self.database.make_search()
@@ -500,7 +508,7 @@ async def post_search(
500508
filter_kwargs = search_request.fields.filter_fields
501509

502510
items = [
503-
json.loads(stac_pydantic.Item(**feat).json(**filter_kwargs))
511+
orjson.loads(stac_pydantic.Item(**feat).json(**filter_kwargs))
504512
for feat in items
505513
]
506514

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
":",
4040
}
4141

42-
DEFAULT_INDICES = f"*,-*kibana*,-{COLLECTIONS_INDEX}"
42+
ITEM_INDICES = f"{ITEMS_INDEX_PREFIX}*,-*kibana*,-{COLLECTIONS_INDEX}*"
4343

4444
DEFAULT_SORT = {
4545
"properties.datetime": {"order": "desc"},
@@ -164,7 +164,7 @@ def indices(collection_ids: Optional[List[str]]) -> str:
164164
A string of comma-separated index names. If `collection_ids` is None, returns the default indices.
165165
"""
166166
if collection_ids is None:
167-
return DEFAULT_INDICES
167+
return ITEM_INDICES
168168
else:
169169
return ",".join([index_by_collection_id(c) for c in collection_ids])
170170

@@ -178,7 +178,8 @@ async def create_collection_index() -> None:
178178
client = AsyncElasticsearchSettings().create_client
179179

180180
await client.indices.create(
181-
index=COLLECTIONS_INDEX,
181+
index=f"{COLLECTIONS_INDEX}-000001",
182+
aliases={COLLECTIONS_INDEX: {}},
182183
mappings=ES_COLLECTIONS_MAPPINGS,
183184
ignore=400, # ignore 400 already exists code
184185
)
@@ -197,9 +198,11 @@ async def create_item_index(collection_id: str):
197198
198199
"""
199200
client = AsyncElasticsearchSettings().create_client
201+
index_name = index_by_collection_id(collection_id)
200202

201203
await client.indices.create(
202-
index=index_by_collection_id(collection_id),
204+
index=f"{index_by_collection_id(collection_id)}-000001",
205+
aliases={index_name: {}},
203206
mappings=ES_ITEMS_MAPPINGS,
204207
settings=ES_ITEMS_SETTINGS,
205208
ignore=400, # ignore 400 already exists code
@@ -215,7 +218,14 @@ async def delete_item_index(collection_id: str):
215218
"""
216219
client = AsyncElasticsearchSettings().create_client
217220

218-
await client.indices.delete(index=index_by_collection_id(collection_id))
221+
name = index_by_collection_id(collection_id)
222+
resolved = await client.indices.resolve_index(name=name)
223+
if "aliases" in resolved and resolved["aliases"]:
224+
[alias] = resolved["aliases"]
225+
await client.indices.delete_alias(index=alias["indices"], name=alias["name"])
226+
await client.indices.delete(index=alias["indices"])
227+
else:
228+
await client.indices.delete(index=name)
219229
await client.close()
220230

221231

@@ -786,14 +796,11 @@ async def bulk_async(
786796
`mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to True, the
787797
index is refreshed after the bulk insert. The function does not return any value.
788798
"""
789-
await asyncio.get_event_loop().run_in_executor(
790-
None,
791-
lambda: helpers.bulk(
792-
self.sync_client,
793-
mk_actions(collection_id, processed_items),
794-
refresh=refresh,
795-
raise_on_error=False,
796-
),
799+
await helpers.async_bulk(
800+
self.client,
801+
mk_actions(collection_id, processed_items),
802+
refresh=refresh,
803+
raise_on_error=False,
797804
)
798805

799806
def bulk_sync(
@@ -824,7 +831,7 @@ def bulk_sync(
824831
async def delete_items(self) -> None:
825832
"""Danger. this is only for tests."""
826833
await self.client.delete_by_query(
827-
index=DEFAULT_INDICES,
834+
index=ITEM_INDICES,
828835
body={"query": {"match_all": {}}},
829836
wait_for_completion=True,
830837
)

stac_fastapi/elasticsearch/tests/api/test_api.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,28 @@ async def test_search_invalid_date(app_client, ctx):
255255

256256

257257
@pytest.mark.asyncio
258-
async def test_search_point_intersects(app_client, ctx):
258+
async def test_search_point_intersects_get(app_client, ctx):
259+
resp = await app_client.get(
260+
'/search?intersects={"type":"Point","coordinates":[150.04,-33.14]}'
261+
)
262+
263+
assert resp.status_code == 200
264+
resp_json = resp.json()
265+
assert len(resp_json["features"]) == 1
266+
267+
268+
@pytest.mark.asyncio
269+
async def test_search_polygon_intersects_get(app_client, ctx):
270+
resp = await app_client.get(
271+
'/search?intersects={"type":"Polygon","coordinates":[[[149.04, -34.14],[149.04, -32.14],[151.04, -32.14],[151.04, -34.14],[149.04, -34.14]]]}'
272+
)
273+
274+
assert resp.status_code == 200
275+
resp_json = resp.json()
276+
assert len(resp_json["features"]) == 1
277+
278+
279+
async def test_search_point_intersects_post(app_client, ctx):
259280
point = [150.04, -33.14]
260281
intersects = {"type": "Point", "coordinates": point}
261282

0 commit comments

Comments
 (0)