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.
@@ -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
- "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators"
60
+ FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS
60
61
  )
61
62
 
62
63
  aggregation_extension = AggregationExtension(
63
- client=EsAsyncAggregationClient(
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
- api = StacApi(
107
- title=os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-elasticsearch"),
108
- description=os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-elasticsearch"),
109
- api_version=os.getenv("STAC_FASTAPI_VERSION", "4.2.0"),
110
- settings=settings,
111
- extensions=extensions,
112
- client=CoreClient(
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=create_get_request_model(search_extensions),
119
- search_post_request_model=post_request_model,
120
- route_dependencies=get_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, validate_refresh
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.database_logic import (
18
- COLLECTIONS_INDEX,
19
- DEFAULT_SORT,
20
- ES_COLLECTIONS_MAPPINGS,
21
- ES_ITEMS_MAPPINGS,
22
- ES_ITEMS_SETTINGS,
23
- ITEM_INDICES,
24
- ITEMS_INDEX_PREFIX,
25
- Geometry,
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.core.extensions import filter
33
- from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
34
- from stac_fastapi.core.utilities import MAX_LIMIT, bbox2polygon, validate_refresh
35
- from stac_fastapi.elasticsearch.config import AsyncElasticsearchSettings
36
- from stac_fastapi.elasticsearch.config import (
37
- ElasticsearchSettings as SyncElasticsearchSettings,
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
- client = AsyncElasticsearchSettings().create_client
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
- name = index_alias_by_collection_id(collection_id)
117
- resolved = await client.indices.resolve_index(name=name)
118
- if "aliases" in resolved and resolved["aliases"]:
119
- [alias] = resolved["aliases"]
120
- await client.indices.delete_alias(index=alias["indices"], name=alias["name"])
121
- await client.indices.delete(index=alias["indices"])
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
- for mapping in mappings.values():
310
- fields = mapping["mappings"].get("properties", {})
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(search: Search, datetime_search: dict):
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 (Search): The search object to filter.
342
- datetime_search (dict): The datetime filter criteria.
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
- Search: The filtered search object.
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
- should.extend(
354
- [
355
- Q(
356
- "bool",
357
- filter=[
358
- Q(
359
- "term",
360
- properties__datetime=datetime_search["eq"],
361
- ),
362
- ],
363
- ),
364
- Q(
365
- "bool",
366
- filter=[
367
- Q(
368
- "range",
369
- properties__start_datetime={
370
- "lte": datetime_search["eq"],
371
- },
372
- ),
373
- Q(
374
- "range",
375
- properties__end_datetime={
376
- "gte": datetime_search["eq"],
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
- should.extend(
391
- [
392
- Q(
393
- "bool",
394
- filter=[
395
- Q(
396
- "range",
397
- properties__datetime={
398
- "gte": datetime_search["gte"],
399
- "lte": datetime_search["lte"],
400
- },
401
- ),
402
- ],
403
- ),
404
- Q(
405
- "bool",
406
- filter=[
407
- Q(
408
- "range",
409
- properties__start_datetime={
410
- "gte": datetime_search["gte"],
411
- "lte": datetime_search["lte"],
412
- },
413
- ),
414
- ],
415
- ),
416
- Q(
417
- "bool",
418
- filter=[
419
- Q(
420
- "range",
421
- properties__end_datetime={
422
- "gte": datetime_search["gte"],
423
- "lte": datetime_search["lte"],
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
- return search.filter(
501
- Q(
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
- """Database logic to perform query for search endpoint."""
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
- return search
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
- """Database logic to sort search instance."""
579
- if sortby:
580
- return {s.field: {"order": s.direction} for s in sortby}
581
- else:
582
- return None
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 = json.loads(urlsafe_b64decode(token).decode())
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(json.dumps(sort_array).encode()).decode()
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__ = "4.2.0"
2
+ __version__ = "5.0.0"