stac-fastapi-elasticsearch 4.2.0__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_fastapi/elasticsearch/app.py +18 -15
- stac_fastapi/elasticsearch/config.py +6 -5
- stac_fastapi/elasticsearch/database_logic.py +181 -261
- stac_fastapi/elasticsearch/version.py +1 -1
- stac_fastapi_elasticsearch-5.0.0.dist-info/METADATA +575 -0
- stac_fastapi_elasticsearch-5.0.0.dist-info/RECORD +10 -0
- stac_fastapi_elasticsearch-4.2.0.dist-info/METADATA +0 -382
- stac_fastapi_elasticsearch-4.2.0.dist-info/RECORD +0 -10
- {stac_fastapi_elasticsearch-4.2.0.dist-info → stac_fastapi_elasticsearch-5.0.0.dist-info}/WHEEL +0 -0
- {stac_fastapi_elasticsearch-4.2.0.dist-info → stac_fastapi_elasticsearch-5.0.0.dist-info}/entry_points.txt +0 -0
- {stac_fastapi_elasticsearch-4.2.0.dist-info → stac_fastapi_elasticsearch-5.0.0.dist-info}/top_level.txt +0 -0
|
@@ -11,14 +11,12 @@ from stac_fastapi.api.models import create_get_request_model, create_post_reques
|
|
|
11
11
|
from stac_fastapi.core.core import (
|
|
12
12
|
BulkTransactionsClient,
|
|
13
13
|
CoreClient,
|
|
14
|
-
EsAsyncBaseFiltersClient,
|
|
15
14
|
TransactionsClient,
|
|
16
15
|
)
|
|
17
16
|
from stac_fastapi.core.extensions import QueryExtension
|
|
18
17
|
from stac_fastapi.core.extensions.aggregation import (
|
|
19
18
|
EsAggregationExtensionGetRequest,
|
|
20
19
|
EsAggregationExtensionPostRequest,
|
|
21
|
-
EsAsyncAggregationClient,
|
|
22
20
|
)
|
|
23
21
|
from stac_fastapi.core.extensions.fields import FieldsExtension
|
|
24
22
|
from stac_fastapi.core.rate_limit import setup_rate_limit
|
|
@@ -39,7 +37,10 @@ from stac_fastapi.extensions.core import (
|
|
|
39
37
|
TokenPaginationExtension,
|
|
40
38
|
TransactionExtension,
|
|
41
39
|
)
|
|
40
|
+
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
|
|
42
41
|
from stac_fastapi.extensions.third_party import BulkTransactionExtension
|
|
42
|
+
from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient
|
|
43
|
+
from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient
|
|
43
44
|
|
|
44
45
|
logging.basicConfig(level=logging.INFO)
|
|
45
46
|
logger = logging.getLogger(__name__)
|
|
@@ -56,11 +57,11 @@ filter_extension = FilterExtension(
|
|
|
56
57
|
client=EsAsyncBaseFiltersClient(database=database_logic)
|
|
57
58
|
)
|
|
58
59
|
filter_extension.conformance_classes.append(
|
|
59
|
-
|
|
60
|
+
FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
aggregation_extension = AggregationExtension(
|
|
63
|
-
client=
|
|
64
|
+
client=EsAsyncBaseAggregationClient(
|
|
64
65
|
database=database_logic, session=session, settings=settings
|
|
65
66
|
)
|
|
66
67
|
)
|
|
@@ -103,22 +104,24 @@ database_logic.extensions = [type(ext).__name__ for ext in extensions]
|
|
|
103
104
|
|
|
104
105
|
post_request_model = create_post_request_model(search_extensions)
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
title
|
|
108
|
-
description
|
|
109
|
-
api_version
|
|
110
|
-
settings
|
|
111
|
-
extensions
|
|
112
|
-
client
|
|
107
|
+
app_config = {
|
|
108
|
+
"title": os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-elasticsearch"),
|
|
109
|
+
"description": os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-elasticsearch"),
|
|
110
|
+
"api_version": os.getenv("STAC_FASTAPI_VERSION", "5.0.0"),
|
|
111
|
+
"settings": settings,
|
|
112
|
+
"extensions": extensions,
|
|
113
|
+
"client": CoreClient(
|
|
113
114
|
database=database_logic,
|
|
114
115
|
session=session,
|
|
115
116
|
post_request_model=post_request_model,
|
|
116
117
|
landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
|
|
117
118
|
),
|
|
118
|
-
search_get_request_model
|
|
119
|
-
search_post_request_model
|
|
120
|
-
route_dependencies
|
|
121
|
-
|
|
119
|
+
"search_get_request_model": create_get_request_model(search_extensions),
|
|
120
|
+
"search_post_request_model": post_request_model,
|
|
121
|
+
"route_dependencies": get_route_dependencies(),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
api = StacApi(**app_config)
|
|
122
125
|
|
|
123
126
|
|
|
124
127
|
@asynccontextmanager
|
|
@@ -10,7 +10,8 @@ from elasticsearch._async.client import AsyncElasticsearch
|
|
|
10
10
|
|
|
11
11
|
from elasticsearch import Elasticsearch # type: ignore[attr-defined]
|
|
12
12
|
from stac_fastapi.core.base_settings import ApiBaseSettings
|
|
13
|
-
from stac_fastapi.core.utilities import get_bool_env
|
|
13
|
+
from stac_fastapi.core.utilities import get_bool_env
|
|
14
|
+
from stac_fastapi.sfeos_helpers.database import validate_refresh
|
|
14
15
|
from stac_fastapi.types.config import ApiSettings
|
|
15
16
|
|
|
16
17
|
|
|
@@ -51,6 +52,10 @@ def _es_config() -> Dict[str, Any]:
|
|
|
51
52
|
if http_compress:
|
|
52
53
|
config["http_compress"] = True
|
|
53
54
|
|
|
55
|
+
# Handle authentication
|
|
56
|
+
if (u := os.getenv("ES_USER")) and (p := os.getenv("ES_PASS")):
|
|
57
|
+
config["http_auth"] = (u, p)
|
|
58
|
+
|
|
54
59
|
# Explicitly exclude SSL settings when not using SSL
|
|
55
60
|
if not use_ssl:
|
|
56
61
|
return config
|
|
@@ -63,10 +68,6 @@ def _es_config() -> Dict[str, Any]:
|
|
|
63
68
|
if config["verify_certs"]:
|
|
64
69
|
config["ca_certs"] = os.getenv("CURL_CA_BUNDLE", certifi.where())
|
|
65
70
|
|
|
66
|
-
# Handle authentication
|
|
67
|
-
if (u := os.getenv("ES_USER")) and (p := os.getenv("ES_PASS")):
|
|
68
|
-
config["http_auth"] = (u, p)
|
|
69
|
-
|
|
70
71
|
return config
|
|
71
72
|
|
|
72
73
|
|
|
@@ -1,42 +1,51 @@
|
|
|
1
1
|
"""Database logic."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
|
7
6
|
from copy import deepcopy
|
|
8
|
-
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
|
|
7
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
|
|
9
8
|
|
|
10
9
|
import attr
|
|
11
10
|
import elasticsearch.helpers as helpers
|
|
11
|
+
import orjson
|
|
12
12
|
from elasticsearch.dsl import Q, Search
|
|
13
13
|
from elasticsearch.exceptions import NotFoundError as ESNotFoundError
|
|
14
14
|
from starlette.requests import Request
|
|
15
15
|
|
|
16
16
|
from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
|
|
17
|
-
from stac_fastapi.core.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
|
|
18
|
+
from stac_fastapi.core.utilities import MAX_LIMIT, bbox2polygon
|
|
19
|
+
from stac_fastapi.elasticsearch.config import AsyncElasticsearchSettings
|
|
20
|
+
from stac_fastapi.elasticsearch.config import (
|
|
21
|
+
ElasticsearchSettings as SyncElasticsearchSettings,
|
|
22
|
+
)
|
|
23
|
+
from stac_fastapi.sfeos_helpers import filter
|
|
24
|
+
from stac_fastapi.sfeos_helpers.database import (
|
|
25
|
+
apply_free_text_filter_shared,
|
|
26
|
+
apply_intersects_filter_shared,
|
|
27
|
+
create_index_templates_shared,
|
|
28
|
+
delete_item_index_shared,
|
|
29
|
+
get_queryables_mapping_shared,
|
|
26
30
|
index_alias_by_collection_id,
|
|
27
31
|
index_by_collection_id,
|
|
28
32
|
indices,
|
|
29
33
|
mk_actions,
|
|
30
34
|
mk_item_id,
|
|
35
|
+
populate_sort_shared,
|
|
36
|
+
return_date,
|
|
37
|
+
validate_refresh,
|
|
31
38
|
)
|
|
32
|
-
from stac_fastapi.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
from stac_fastapi.sfeos_helpers.mappings import (
|
|
40
|
+
AGGREGATION_MAPPING,
|
|
41
|
+
COLLECTIONS_INDEX,
|
|
42
|
+
DEFAULT_SORT,
|
|
43
|
+
ITEM_INDICES,
|
|
44
|
+
ITEMS_INDEX_PREFIX,
|
|
45
|
+
Geometry,
|
|
38
46
|
)
|
|
39
47
|
from stac_fastapi.types.errors import ConflictError, NotFoundError
|
|
48
|
+
from stac_fastapi.types.rfc3339 import DateTimeType
|
|
40
49
|
from stac_fastapi.types.stac import Collection, Item
|
|
41
50
|
|
|
42
51
|
logger = logging.getLogger(__name__)
|
|
@@ -50,22 +59,7 @@ async def create_index_templates() -> None:
|
|
|
50
59
|
None
|
|
51
60
|
|
|
52
61
|
"""
|
|
53
|
-
|
|
54
|
-
await client.indices.put_index_template(
|
|
55
|
-
name=f"template_{COLLECTIONS_INDEX}",
|
|
56
|
-
body={
|
|
57
|
-
"index_patterns": [f"{COLLECTIONS_INDEX}*"],
|
|
58
|
-
"template": {"mappings": ES_COLLECTIONS_MAPPINGS},
|
|
59
|
-
},
|
|
60
|
-
)
|
|
61
|
-
await client.indices.put_index_template(
|
|
62
|
-
name=f"template_{ITEMS_INDEX_PREFIX}",
|
|
63
|
-
body={
|
|
64
|
-
"index_patterns": [f"{ITEMS_INDEX_PREFIX}*"],
|
|
65
|
-
"template": {"settings": ES_ITEMS_SETTINGS, "mappings": ES_ITEMS_MAPPINGS},
|
|
66
|
-
},
|
|
67
|
-
)
|
|
68
|
-
await client.close()
|
|
62
|
+
await create_index_templates_shared(settings=AsyncElasticsearchSettings())
|
|
69
63
|
|
|
70
64
|
|
|
71
65
|
async def create_collection_index() -> None:
|
|
@@ -110,18 +104,13 @@ async def delete_item_index(collection_id: str):
|
|
|
110
104
|
|
|
111
105
|
Args:
|
|
112
106
|
collection_id (str): The ID of the collection whose items index will be deleted.
|
|
113
|
-
"""
|
|
114
|
-
client = AsyncElasticsearchSettings().create_client
|
|
115
107
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
else:
|
|
123
|
-
await client.indices.delete(index=name)
|
|
124
|
-
await client.close()
|
|
108
|
+
Notes:
|
|
109
|
+
This function delegates to the shared implementation in delete_item_index_shared.
|
|
110
|
+
"""
|
|
111
|
+
await delete_item_index_shared(
|
|
112
|
+
settings=AsyncElasticsearchSettings(), collection_id=collection_id
|
|
113
|
+
)
|
|
125
114
|
|
|
126
115
|
|
|
127
116
|
@attr.s
|
|
@@ -150,76 +139,7 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
150
139
|
|
|
151
140
|
extensions: List[str] = attr.ib(default=attr.Factory(list))
|
|
152
141
|
|
|
153
|
-
aggregation_mapping: Dict[str, Dict[str, Any]] =
|
|
154
|
-
"total_count": {"value_count": {"field": "id"}},
|
|
155
|
-
"collection_frequency": {"terms": {"field": "collection", "size": 100}},
|
|
156
|
-
"platform_frequency": {"terms": {"field": "properties.platform", "size": 100}},
|
|
157
|
-
"cloud_cover_frequency": {
|
|
158
|
-
"range": {
|
|
159
|
-
"field": "properties.eo:cloud_cover",
|
|
160
|
-
"ranges": [
|
|
161
|
-
{"to": 5},
|
|
162
|
-
{"from": 5, "to": 15},
|
|
163
|
-
{"from": 15, "to": 40},
|
|
164
|
-
{"from": 40},
|
|
165
|
-
],
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
"datetime_frequency": {
|
|
169
|
-
"date_histogram": {
|
|
170
|
-
"field": "properties.datetime",
|
|
171
|
-
"calendar_interval": "month",
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
"datetime_min": {"min": {"field": "properties.datetime"}},
|
|
175
|
-
"datetime_max": {"max": {"field": "properties.datetime"}},
|
|
176
|
-
"grid_code_frequency": {
|
|
177
|
-
"terms": {
|
|
178
|
-
"field": "properties.grid:code",
|
|
179
|
-
"missing": "none",
|
|
180
|
-
"size": 10000,
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
"sun_elevation_frequency": {
|
|
184
|
-
"histogram": {"field": "properties.view:sun_elevation", "interval": 5}
|
|
185
|
-
},
|
|
186
|
-
"sun_azimuth_frequency": {
|
|
187
|
-
"histogram": {"field": "properties.view:sun_azimuth", "interval": 5}
|
|
188
|
-
},
|
|
189
|
-
"off_nadir_frequency": {
|
|
190
|
-
"histogram": {"field": "properties.view:off_nadir", "interval": 5}
|
|
191
|
-
},
|
|
192
|
-
"centroid_geohash_grid_frequency": {
|
|
193
|
-
"geohash_grid": {
|
|
194
|
-
"field": "properties.proj:centroid",
|
|
195
|
-
"precision": 1,
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
"centroid_geohex_grid_frequency": {
|
|
199
|
-
"geohex_grid": {
|
|
200
|
-
"field": "properties.proj:centroid",
|
|
201
|
-
"precision": 0,
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
"centroid_geotile_grid_frequency": {
|
|
205
|
-
"geotile_grid": {
|
|
206
|
-
"field": "properties.proj:centroid",
|
|
207
|
-
"precision": 0,
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
"geometry_geohash_grid_frequency": {
|
|
211
|
-
"geohash_grid": {
|
|
212
|
-
"field": "geometry",
|
|
213
|
-
"precision": 1,
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
"geometry_geotile_grid_frequency": {
|
|
217
|
-
"geotile_grid": {
|
|
218
|
-
"field": "geometry",
|
|
219
|
-
"precision": 0,
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
}
|
|
142
|
+
aggregation_mapping: Dict[str, Dict[str, Any]] = AGGREGATION_MAPPING
|
|
223
143
|
|
|
224
144
|
"""CORE LOGIC"""
|
|
225
145
|
|
|
@@ -300,23 +220,12 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
300
220
|
Returns:
|
|
301
221
|
dict: A dictionary containing the Queryables mappings.
|
|
302
222
|
"""
|
|
303
|
-
queryables_mapping = {}
|
|
304
|
-
|
|
305
223
|
mappings = await self.client.indices.get_mapping(
|
|
306
224
|
index=f"{ITEMS_INDEX_PREFIX}{collection_id}",
|
|
307
225
|
)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
properties = fields.pop("properties", {}).get("properties", {}).keys()
|
|
312
|
-
|
|
313
|
-
for field_key in fields:
|
|
314
|
-
queryables_mapping[field_key] = field_key
|
|
315
|
-
|
|
316
|
-
for property_key in properties:
|
|
317
|
-
queryables_mapping[property_key] = f"properties.{property_key}"
|
|
318
|
-
|
|
319
|
-
return queryables_mapping
|
|
226
|
+
return await get_queryables_mapping_shared(
|
|
227
|
+
collection_id=collection_id, mappings=mappings
|
|
228
|
+
)
|
|
320
229
|
|
|
321
230
|
@staticmethod
|
|
322
231
|
def make_search():
|
|
@@ -334,120 +243,99 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
334
243
|
return search.filter("terms", collection=collection_ids)
|
|
335
244
|
|
|
336
245
|
@staticmethod
|
|
337
|
-
def apply_datetime_filter(
|
|
246
|
+
def apply_datetime_filter(
|
|
247
|
+
search: Search, interval: Optional[Union[DateTimeType, str]]
|
|
248
|
+
) -> Search:
|
|
338
249
|
"""Apply a filter to search on datetime, start_datetime, and end_datetime fields.
|
|
339
250
|
|
|
340
251
|
Args:
|
|
341
|
-
search
|
|
342
|
-
|
|
252
|
+
search: The search object to filter.
|
|
253
|
+
interval: Optional datetime interval to filter by. Can be:
|
|
254
|
+
- A single datetime string (e.g., "2023-01-01T12:00:00")
|
|
255
|
+
- A datetime range string (e.g., "2023-01-01/2023-12-31")
|
|
256
|
+
- A datetime object
|
|
257
|
+
- A tuple of (start_datetime, end_datetime)
|
|
343
258
|
|
|
344
259
|
Returns:
|
|
345
|
-
|
|
260
|
+
The filtered search object.
|
|
346
261
|
"""
|
|
262
|
+
if not interval:
|
|
263
|
+
return search
|
|
264
|
+
|
|
347
265
|
should = []
|
|
266
|
+
try:
|
|
267
|
+
datetime_search = return_date(interval)
|
|
268
|
+
except (ValueError, TypeError) as e:
|
|
269
|
+
# Handle invalid interval formats if return_date fails
|
|
270
|
+
logger.error(f"Invalid interval format: {interval}, error: {e}")
|
|
271
|
+
return search
|
|
348
272
|
|
|
349
|
-
# If the request is a single datetime return
|
|
350
|
-
# items with datetimes equal to the requested datetime OR
|
|
351
|
-
# the requested datetime is between their start and end datetimes
|
|
352
273
|
if "eq" in datetime_search:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
]
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
# If the request is a date range return
|
|
385
|
-
# items with datetimes within the requested date range OR
|
|
386
|
-
# their startdatetime ithin the requested date range OR
|
|
387
|
-
# their enddatetime ithin the requested date range OR
|
|
388
|
-
# the requested daterange within their start and end datetimes
|
|
274
|
+
# For exact matches, include:
|
|
275
|
+
# 1. Items with matching exact datetime
|
|
276
|
+
# 2. Items with datetime:null where the time falls within their range
|
|
277
|
+
should = [
|
|
278
|
+
Q(
|
|
279
|
+
"bool",
|
|
280
|
+
filter=[
|
|
281
|
+
Q("exists", field="properties.datetime"),
|
|
282
|
+
Q("term", **{"properties__datetime": datetime_search["eq"]}),
|
|
283
|
+
],
|
|
284
|
+
),
|
|
285
|
+
Q(
|
|
286
|
+
"bool",
|
|
287
|
+
must_not=[Q("exists", field="properties.datetime")],
|
|
288
|
+
filter=[
|
|
289
|
+
Q("exists", field="properties.start_datetime"),
|
|
290
|
+
Q("exists", field="properties.end_datetime"),
|
|
291
|
+
Q(
|
|
292
|
+
"range",
|
|
293
|
+
properties__start_datetime={"lte": datetime_search["eq"]},
|
|
294
|
+
),
|
|
295
|
+
Q(
|
|
296
|
+
"range",
|
|
297
|
+
properties__end_datetime={"gte": datetime_search["eq"]},
|
|
298
|
+
),
|
|
299
|
+
],
|
|
300
|
+
),
|
|
301
|
+
]
|
|
389
302
|
else:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
],
|
|
427
|
-
),
|
|
428
|
-
Q(
|
|
429
|
-
"bool",
|
|
430
|
-
filter=[
|
|
431
|
-
Q(
|
|
432
|
-
"range",
|
|
433
|
-
properties__start_datetime={
|
|
434
|
-
"lte": datetime_search["gte"]
|
|
435
|
-
},
|
|
436
|
-
),
|
|
437
|
-
Q(
|
|
438
|
-
"range",
|
|
439
|
-
properties__end_datetime={
|
|
440
|
-
"gte": datetime_search["lte"]
|
|
441
|
-
},
|
|
442
|
-
),
|
|
443
|
-
],
|
|
444
|
-
),
|
|
445
|
-
]
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
search = search.query(Q("bool", filter=[Q("bool", should=should)]))
|
|
449
|
-
|
|
450
|
-
return search
|
|
303
|
+
# For date ranges, include:
|
|
304
|
+
# 1. Items with datetime in the range
|
|
305
|
+
# 2. Items with datetime:null that overlap the search range
|
|
306
|
+
should = [
|
|
307
|
+
Q(
|
|
308
|
+
"bool",
|
|
309
|
+
filter=[
|
|
310
|
+
Q("exists", field="properties.datetime"),
|
|
311
|
+
Q(
|
|
312
|
+
"range",
|
|
313
|
+
properties__datetime={
|
|
314
|
+
"gte": datetime_search["gte"],
|
|
315
|
+
"lte": datetime_search["lte"],
|
|
316
|
+
},
|
|
317
|
+
),
|
|
318
|
+
],
|
|
319
|
+
),
|
|
320
|
+
Q(
|
|
321
|
+
"bool",
|
|
322
|
+
must_not=[Q("exists", field="properties.datetime")],
|
|
323
|
+
filter=[
|
|
324
|
+
Q("exists", field="properties.start_datetime"),
|
|
325
|
+
Q("exists", field="properties.end_datetime"),
|
|
326
|
+
Q(
|
|
327
|
+
"range",
|
|
328
|
+
properties__start_datetime={"lte": datetime_search["lte"]},
|
|
329
|
+
),
|
|
330
|
+
Q(
|
|
331
|
+
"range",
|
|
332
|
+
properties__end_datetime={"gte": datetime_search["gte"]},
|
|
333
|
+
),
|
|
334
|
+
],
|
|
335
|
+
),
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
return search.query(Q("bool", should=should, minimum_should_match=1))
|
|
451
339
|
|
|
452
340
|
@staticmethod
|
|
453
341
|
def apply_bbox_filter(search: Search, bbox: List):
|
|
@@ -497,21 +385,8 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
497
385
|
Notes:
|
|
498
386
|
A geo_shape filter is added to the search object, set to intersect with the specified geometry.
|
|
499
387
|
"""
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
{
|
|
503
|
-
"geo_shape": {
|
|
504
|
-
"geometry": {
|
|
505
|
-
"shape": {
|
|
506
|
-
"type": intersects.type.lower(),
|
|
507
|
-
"coordinates": intersects.coordinates,
|
|
508
|
-
},
|
|
509
|
-
"relation": "intersects",
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
)
|
|
514
|
-
)
|
|
388
|
+
filter = apply_intersects_filter_shared(intersects=intersects)
|
|
389
|
+
return search.filter(Q(filter))
|
|
515
390
|
|
|
516
391
|
@staticmethod
|
|
517
392
|
def apply_stacql_filter(search: Search, op: str, field: str, value: float):
|
|
@@ -537,14 +412,21 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
537
412
|
|
|
538
413
|
@staticmethod
|
|
539
414
|
def apply_free_text_filter(search: Search, free_text_queries: Optional[List[str]]):
|
|
540
|
-
"""
|
|
541
|
-
if free_text_queries is not None:
|
|
542
|
-
free_text_query_string = '" OR properties.\\*:"'.join(free_text_queries)
|
|
543
|
-
search = search.query(
|
|
544
|
-
"query_string", query=f'properties.\\*:"{free_text_query_string}"'
|
|
545
|
-
)
|
|
415
|
+
"""Create a free text query for Elasticsearch queries.
|
|
546
416
|
|
|
547
|
-
|
|
417
|
+
This method delegates to the shared implementation in apply_free_text_filter_shared.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
search (Search): The search object to apply the query to.
|
|
421
|
+
free_text_queries (Optional[List[str]]): A list of text strings to search for in the properties.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Search: The search object with the free text query applied, or the original search
|
|
425
|
+
object if no free_text_queries were provided.
|
|
426
|
+
"""
|
|
427
|
+
return apply_free_text_filter_shared(
|
|
428
|
+
search=search, free_text_queries=free_text_queries
|
|
429
|
+
)
|
|
548
430
|
|
|
549
431
|
async def apply_cql2_filter(
|
|
550
432
|
self, search: Search, _filter: Optional[Dict[str, Any]]
|
|
@@ -575,11 +457,18 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
575
457
|
|
|
576
458
|
@staticmethod
|
|
577
459
|
def populate_sort(sortby: List) -> Optional[Dict[str, Dict[str, str]]]:
|
|
578
|
-
"""
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
460
|
+
"""Create a sort configuration for Elasticsearch queries.
|
|
461
|
+
|
|
462
|
+
This method delegates to the shared implementation in populate_sort_shared.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
sortby (List): A list of sort specifications, each containing a field and direction.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Optional[Dict[str, Dict[str, str]]]: A dictionary mapping field names to sort direction
|
|
469
|
+
configurations, or None if no sort was specified.
|
|
470
|
+
"""
|
|
471
|
+
return populate_sort_shared(sortby=sortby)
|
|
583
472
|
|
|
584
473
|
async def execute_search(
|
|
585
474
|
self,
|
|
@@ -614,7 +503,7 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
614
503
|
search_after = None
|
|
615
504
|
|
|
616
505
|
if token:
|
|
617
|
-
search_after =
|
|
506
|
+
search_after = orjson.loads(urlsafe_b64decode(token))
|
|
618
507
|
|
|
619
508
|
query = search.query.to_dict() if search.query else None
|
|
620
509
|
|
|
@@ -654,7 +543,7 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
654
543
|
next_token = None
|
|
655
544
|
if len(hits) > limit and limit < max_result_window:
|
|
656
545
|
if hits and (sort_array := hits[limit - 1].get("sort")):
|
|
657
|
-
next_token = urlsafe_b64encode(
|
|
546
|
+
next_token = urlsafe_b64encode(orjson.dumps(sort_array)).decode()
|
|
658
547
|
|
|
659
548
|
matched = (
|
|
660
549
|
es_response["hits"]["total"]["value"]
|
|
@@ -982,6 +871,37 @@ class DatabaseLogic(BaseDatabaseLogic):
|
|
|
982
871
|
except ESNotFoundError:
|
|
983
872
|
raise NotFoundError(f"Mapping for index {index_name} not found")
|
|
984
873
|
|
|
874
|
+
async def get_items_unique_values(
|
|
875
|
+
self, collection_id: str, field_names: Iterable[str], *, limit: int = 100
|
|
876
|
+
) -> Dict[str, List[str]]:
|
|
877
|
+
"""Get the unique values for the given fields in the collection."""
|
|
878
|
+
limit_plus_one = limit + 1
|
|
879
|
+
index_name = index_alias_by_collection_id(collection_id)
|
|
880
|
+
|
|
881
|
+
query = await self.client.search(
|
|
882
|
+
index=index_name,
|
|
883
|
+
body={
|
|
884
|
+
"size": 0,
|
|
885
|
+
"aggs": {
|
|
886
|
+
field: {"terms": {"field": field, "size": limit_plus_one}}
|
|
887
|
+
for field in field_names
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
result: Dict[str, List[str]] = {}
|
|
893
|
+
for field, agg in query["aggregations"].items():
|
|
894
|
+
if len(agg["buckets"]) > limit:
|
|
895
|
+
logger.warning(
|
|
896
|
+
"Skipping enum field %s: exceeds limit of %d unique values. "
|
|
897
|
+
"Consider excluding this field from enumeration or increase the limit.",
|
|
898
|
+
field,
|
|
899
|
+
limit,
|
|
900
|
+
)
|
|
901
|
+
continue
|
|
902
|
+
result[field] = [bucket["key"] for bucket in agg["buckets"]]
|
|
903
|
+
return result
|
|
904
|
+
|
|
985
905
|
async def create_collection(self, collection: Collection, **kwargs: Any):
|
|
986
906
|
"""Create a single collection in the database.
|
|
987
907
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""library version."""
|
|
2
|
-
__version__ = "
|
|
2
|
+
__version__ = "5.0.0"
|