Skip to content

Commit 10bb44c

Browse files
authored
Merge branch 'main' into release/6.4.0
2 parents 0eba51e + 0988448 commit 10bb44c

File tree

9 files changed

+386
-44
lines changed

9 files changed

+386
-44
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1818

1919
### Added
2020

21+
- GET `/collections` collection search free text extension ex. `/collections?q=sentinel`. [#470](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/470)
2122
- Added `USE_DATETIME` environment variable to configure datetime search behavior in SFEOS. [#452](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/452)
2223
- GET `/collections` collection search sort extension ex. `/collections?sortby=+id`. [#456](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/456)
2324
- GET `/collections` collection search fields extension ex. `/collections?fields=id,title`. [#465](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/465)
2425
- Improved error messages for sorting on unsortable fields in collection search, including guidance on how to make fields sortable. [#465](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/465)
2526
- Added field alias for `temporal` to enable easier sorting by temporal extent, alongside `extent.temporal.interval`. [#465](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/465)
27+
- Added `ENABLE_COLLECTIONS_SEARCH` environment variable to make collection search extensions optional (defaults to enabled). [#465](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/465)
2628

2729
### Changed
2830

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,15 @@ SFEOS implements extended capabilities for the `/collections` endpoint, allowing
126126
- Example: `/collections?fields=id,title,description`
127127
- This helps reduce payload size when only certain fields are needed
128128

129+
- **Free Text Search**: Search across collection text fields using the `q` parameter
130+
- Example: `/collections?q=landsat`
131+
- Searches across multiple text fields including title, description, and keywords
132+
- Supports partial word matching and relevance-based sorting
133+
129134
These extensions make it easier to build user interfaces that display and navigate through collections efficiently.
130135

136+
> **Configuration**: Collection search extensions can be disabled by setting the `ENABLE_COLLECTIONS_SEARCH` environment variable to `false`. By default, these extensions are enabled.
137+
131138
> **Note**: Sorting is only available on fields that are indexed for sorting in Elasticsearch/OpenSearch. With the default mappings, you can sort on:
132139
> - `id` (keyword field)
133140
> - `extent.temporal.interval` (date field)
@@ -267,6 +274,7 @@ You can customize additional settings in your `.env` file:
267274
| `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
268275
| `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` | Optional |
269276
| `DATABASE_REFRESH` | Controls whether database operations refresh the index immediately after changes. If set to `true`, changes will be immediately searchable. If set to `false`, changes may not be immediately visible but can improve performance for bulk operations. If set to `wait_for`, changes will wait for the next refresh cycle to become visible. | `false` | Optional |
277+
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields). | `true` | Optional |
270278
| `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. If set to `false`, the POST `/collections` route and related transaction endpoints (including bulk transaction operations) will be unavailable in the API. This is useful for deployments where mutating the catalog via the API should be prevented. | `true` | Optional |
271279
| `STAC_ITEM_LIMIT` | Sets the environment variable for result limiting to SFEOS for the number of returned items and STAC collections. | `10` | Optional |
272280
| `STAC_INDEX_ASSETS` | Controls if Assets are indexed when added to Elasticsearch/Opensearch. This allows asset fields to be included in search queries. | `false` | Optional |
@@ -413,6 +421,10 @@ The system uses a precise naming convention:
413421
- **Root Path Configuration**: The application root path is the base URL by default.
414422
- For AWS Lambda with Gateway API: Set `STAC_FASTAPI_ROOT_PATH` to match the Gateway API stage name (e.g., `/v1`)
415423

424+
- **Feature Configuration**: Control which features are enabled:
425+
- `ENABLE_COLLECTIONS_SEARCH`: Set to `true` (default) to enable collection search extensions (sort, fields). Set to `false` to disable.
426+
- `ENABLE_TRANSACTIONS_EXTENSIONS`: Set to `true` (default) to enable transaction extensions. Set to `false` to disable.
427+
416428

417429
## Collection Pagination
418430

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,18 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
225225
return landing_page
226226

227227
async def all_collections(
228-
self, fields: Optional[List[str]] = None, sortby: Optional[str] = None, **kwargs
228+
self,
229+
fields: Optional[List[str]] = None,
230+
sortby: Optional[str] = None,
231+
q: Optional[Union[str, List[str]]] = None,
232+
**kwargs,
229233
) -> stac_types.Collections:
230234
"""Read all collections from the database.
231235
232236
Args:
233237
fields (Optional[List[str]]): Fields to include or exclude from the results.
234238
sortby (Optional[str]): Sorting options for the results.
239+
q (Optional[List[str]]): Free text search terms.
235240
**kwargs: Keyword arguments from the request.
236241
237242
Returns:
@@ -266,8 +271,13 @@ async def all_collections(
266271
if parsed_sort:
267272
sort = parsed_sort
268273

274+
# Convert q to a list if it's a string
275+
q_list = None
276+
if q is not None:
277+
q_list = [q] if isinstance(q, str) else q
278+
269279
collections, next_token = await self.database.get_all_collections(
270-
token=token, limit=limit, request=request, sort=sort
280+
token=token, limit=limit, request=request, sort=sort, q=q_list
271281
)
272282

273283
# Apply field filtering if fields parameter was provided

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@
4545
)
4646
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
4747
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
48-
49-
# from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
48+
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
5049
from stac_fastapi.extensions.core.query import QueryConformanceClasses
5150
from stac_fastapi.extensions.core.sort import SortConformanceClasses
5251
from stac_fastapi.extensions.third_party import BulkTransactionExtension
@@ -57,7 +56,9 @@
5756
logger = logging.getLogger(__name__)
5857

5958
TRANSACTIONS_EXTENSIONS = get_bool_env("ENABLE_TRANSACTIONS_EXTENSIONS", default=True)
59+
ENABLE_COLLECTIONS_SEARCH = get_bool_env("ENABLE_COLLECTIONS_SEARCH", default=True)
6060
logger.info("TRANSACTIONS_EXTENSIONS is set to %s", TRANSACTIONS_EXTENSIONS)
61+
logger.info("ENABLE_COLLECTIONS_SEARCH is set to %s", ENABLE_COLLECTIONS_SEARCH)
6162

6263
settings = ElasticsearchSettings()
6364
session = Session.create_from_settings(settings)
@@ -115,25 +116,26 @@
115116

116117
extensions = [aggregation_extension] + search_extensions
117118

118-
# Create collection search extensions
119-
# Only sort extension is enabled for now
120-
collection_search_extensions = [
121-
# QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
122-
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
123-
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
124-
# CollectionSearchFilterExtension(
125-
# conformance_classes=[FilterConformanceClasses.COLLECTIONS]
126-
# ),
127-
# FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]),
128-
]
129-
130-
# Initialize collection search with its extensions
131-
collection_search_ext = CollectionSearchExtension.from_extensions(
132-
collection_search_extensions
133-
)
134-
collections_get_request_model = collection_search_ext.GET
119+
# Create collection search extensions if enabled
120+
if ENABLE_COLLECTIONS_SEARCH:
121+
# Create collection search extensions
122+
collection_search_extensions = [
123+
# QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
124+
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
125+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
126+
# CollectionSearchFilterExtension(
127+
# conformance_classes=[FilterConformanceClasses.COLLECTIONS]
128+
# ),
129+
FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]),
130+
]
131+
132+
# Initialize collection search with its extensions
133+
collection_search_ext = CollectionSearchExtension.from_extensions(
134+
collection_search_extensions
135+
)
136+
collections_get_request_model = collection_search_ext.GET
135137

136-
extensions.append(collection_search_ext)
138+
extensions.append(collection_search_ext)
137139

138140
database_logic.extensions = [type(ext).__name__ for ext in extensions]
139141

@@ -170,10 +172,13 @@
170172
"search_get_request_model": create_get_request_model(search_extensions),
171173
"search_post_request_model": post_request_model,
172174
"items_get_request_model": items_get_request_model,
173-
"collections_get_request_model": collections_get_request_model,
174175
"route_dependencies": get_route_dependencies(),
175176
}
176177

178+
# Add collections_get_request_model if collection search is enabled
179+
if ENABLE_COLLECTIONS_SEARCH:
180+
app_config["collections_get_request_model"] = collections_get_request_model
181+
177182
api = StacApi(**app_config)
178183

179184

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ async def get_all_collections(
175175
limit: int,
176176
request: Request,
177177
sort: Optional[List[Dict[str, Any]]] = None,
178+
q: Optional[List[str]] = None,
178179
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
179180
"""Retrieve a list of collections from Elasticsearch, supporting pagination.
180181
@@ -183,6 +184,7 @@ async def get_all_collections(
183184
limit (int): The number of results to return.
184185
request (Request): The FastAPI request object.
185186
sort (Optional[List[Dict[str, Any]]]): Optional sort parameter from the request.
187+
q (Optional[List[str]]): Free text search terms.
186188
187189
Returns:
188190
A tuple of (collections, next pagination token if any).
@@ -223,6 +225,38 @@ async def get_all_collections(
223225
if token:
224226
body["search_after"] = [token]
225227

228+
# Apply free text query if provided
229+
if q:
230+
# For collections, we want to search across all relevant fields
231+
should_clauses = []
232+
233+
# For each search term
234+
for term in q:
235+
# Create a multi_match query for each term
236+
for field in [
237+
"id",
238+
"title",
239+
"description",
240+
"keywords",
241+
"summaries.platform",
242+
"summaries.constellation",
243+
"providers.name",
244+
"providers.url",
245+
]:
246+
should_clauses.append(
247+
{
248+
"wildcard": {
249+
field: {"value": f"*{term}*", "case_insensitive": True}
250+
}
251+
}
252+
)
253+
254+
# Add the query to the body using bool query with should clauses
255+
body["query"] = {
256+
"bool": {"should": should_clauses, "minimum_should_match": 1}
257+
}
258+
259+
# Execute the search
226260
response = await self.client.search(
227261
index=COLLECTIONS_INDEX,
228262
body=body,

stac_fastapi/opensearch/stac_fastapi/opensearch/app.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@
3939
)
4040
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
4141
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
42-
43-
# from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
42+
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
4443
from stac_fastapi.extensions.core.query import QueryConformanceClasses
4544
from stac_fastapi.extensions.core.sort import SortConformanceClasses
4645
from stac_fastapi.extensions.third_party import BulkTransactionExtension
@@ -57,7 +56,9 @@
5756
logger = logging.getLogger(__name__)
5857

5958
TRANSACTIONS_EXTENSIONS = get_bool_env("ENABLE_TRANSACTIONS_EXTENSIONS", default=True)
59+
ENABLE_COLLECTIONS_SEARCH = get_bool_env("ENABLE_COLLECTIONS_SEARCH", default=True)
6060
logger.info("TRANSACTIONS_EXTENSIONS is set to %s", TRANSACTIONS_EXTENSIONS)
61+
logger.info("ENABLE_COLLECTIONS_SEARCH is set to %s", ENABLE_COLLECTIONS_SEARCH)
6162

6263
settings = OpensearchSettings()
6364
session = Session.create_from_settings(settings)
@@ -115,25 +116,26 @@
115116

116117
extensions = [aggregation_extension] + search_extensions
117118

118-
# Create collection search extensions
119-
# Only sort extension is enabled for now
120-
collection_search_extensions = [
121-
# QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
122-
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
123-
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
124-
# CollectionSearchFilterExtension(
125-
# conformance_classes=[FilterConformanceClasses.COLLECTIONS]
126-
# ),
127-
# FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]),
128-
]
129-
130-
# Initialize collection search with its extensions
131-
collection_search_ext = CollectionSearchExtension.from_extensions(
132-
collection_search_extensions
133-
)
134-
collections_get_request_model = collection_search_ext.GET
119+
# Create collection search extensions if enabled
120+
if ENABLE_COLLECTIONS_SEARCH:
121+
# Create collection search extensions
122+
collection_search_extensions = [
123+
# QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
124+
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
125+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
126+
# CollectionSearchFilterExtension(
127+
# conformance_classes=[FilterConformanceClasses.COLLECTIONS]
128+
# ),
129+
FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]),
130+
]
131+
132+
# Initialize collection search with its extensions
133+
collection_search_ext = CollectionSearchExtension.from_extensions(
134+
collection_search_extensions
135+
)
136+
collections_get_request_model = collection_search_ext.GET
135137

136-
extensions.append(collection_search_ext)
138+
extensions.append(collection_search_ext)
137139

138140
database_logic.extensions = [type(ext).__name__ for ext in extensions]
139141

@@ -167,13 +169,16 @@
167169
post_request_model=post_request_model,
168170
landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
169171
),
170-
"collections_get_request_model": collections_get_request_model,
171172
"search_get_request_model": create_get_request_model(search_extensions),
172173
"search_post_request_model": post_request_model,
173174
"items_get_request_model": items_get_request_model,
174175
"route_dependencies": get_route_dependencies(),
175176
}
176177

178+
# Add collections_get_request_model if collection search is enabled
179+
if ENABLE_COLLECTIONS_SEARCH:
180+
app_config["collections_get_request_model"] = collections_get_request_model
181+
177182
api = StacApi(**app_config)
178183

179184

stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ async def get_all_collections(
159159
limit: int,
160160
request: Request,
161161
sort: Optional[List[Dict[str, Any]]] = None,
162+
q: Optional[List[str]] = None,
162163
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
163164
"""Retrieve a list of collections from Elasticsearch, supporting pagination.
164165
@@ -167,6 +168,7 @@ async def get_all_collections(
167168
limit (int): The number of results to return.
168169
request (Request): The FastAPI request object.
169170
sort (Optional[List[Dict[str, Any]]]): Optional sort parameter from the request.
171+
q (Optional[List[str]]): Free text search terms.
170172
171173
Returns:
172174
A tuple of (collections, next pagination token if any).
@@ -207,6 +209,37 @@ async def get_all_collections(
207209
if token:
208210
body["search_after"] = [token]
209211

212+
# Apply free text query if provided
213+
if q:
214+
# For collections, we want to search across all relevant fields
215+
should_clauses = []
216+
217+
# For each search term
218+
for term in q:
219+
# Create a multi_match query for each term
220+
for field in [
221+
"id",
222+
"title",
223+
"description",
224+
"keywords",
225+
"summaries.platform",
226+
"summaries.constellation",
227+
"providers.name",
228+
"providers.url",
229+
]:
230+
should_clauses.append(
231+
{
232+
"wildcard": {
233+
field: {"value": f"*{term}*", "case_insensitive": True}
234+
}
235+
}
236+
)
237+
238+
# Add the query to the body using bool query with should clauses
239+
body["query"] = {
240+
"bool": {"should": should_clauses, "minimum_should_match": 1}
241+
}
242+
210243
response = await self.client.search(
211244
index=COLLECTIONS_INDEX,
212245
body=body,

0 commit comments

Comments
 (0)