Skip to content

Commit 3dbdf23

Browse files
YuriZmytrakovYuri Zmytrakovjonhealy1
authored
feat: Add Redis caching for navigation pagination (stac-utils#488)
**Related Issue(s):** - stac-utils#439 **Description:** Add Redis caching support for navigation pagination to enable proper `prev`/`next` links in STAC API responses. **PR Checklist:** - [x] Code is formatted and linted (run `pre-commit run --all-files`) - [x] Tests pass (run `make test`) - [x] Documentation has been updated to reflect changes, if applicable - [x] Changes are added to the changelog --------- Co-authored-by: Yuri Zmytrakov <[email protected]> Co-authored-by: Jonathan Healy <[email protected]>
1 parent b0e39d6 commit 3dbdf23

File tree

12 files changed

+538
-5
lines changed

12 files changed

+538
-5
lines changed

.github/workflows/cicd.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ jobs:
5656
--ulimit nofile=65536:65536
5757
--ulimit memlock=-1:-1
5858
59+
redis:
60+
image: redis:7-alpine
61+
options: >-
62+
--health-cmd "redis-cli ping"
63+
--health-interval 10s
64+
--health-timeout 5s
65+
--health-retries 5
66+
ports:
67+
- 6379:6379
68+
5969
strategy:
6070
matrix:
6171
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
@@ -115,4 +125,7 @@ jobs:
115125
ES_USE_SSL: false
116126
DATABASE_REFRESH: true
117127
ES_VERIFY_CERTS: false
118-
BACKEND: ${{ matrix.backend == 'elasticsearch8' && 'elasticsearch' || 'opensearch' }}
128+
REDIS_ENABLE: true
129+
REDIS_HOST: localhost
130+
REDIS_PORT: 6379
131+
BACKEND: ${{ matrix.backend == 'elasticsearch8' && 'elasticsearch' || 'opensearch' }}

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ repos:
3131
]
3232
additional_dependencies: [
3333
"types-attrs",
34-
"types-requests"
34+
"types-requests",
35+
"types-redis"
3536
]
3637
- repo: https://github.com/PyCQA/pydocstyle
3738
rev: 6.1.1

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
- Environment variable `EXCLUDED_FROM_QUERYABLES` to exclude specific fields from queryables endpoint and filtering. Supports comma-separated list of fully qualified field names (e.g., `properties.auth:schemes,properties.storage:schemes`) [#489](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/489)
13+
- Added Redis caching configuration for navigation pagination support, enabling proper `prev` and `next` links in paginated responses. [#488](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/488)
1314

1415
### Changed
1516

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,4 @@ docs-image:
117117
.PHONY: docs
118118
docs: docs-image
119119
docker compose -f compose.docs.yml \
120-
run docs
120+
run docs

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
118118
- [Collection Pagination](#collection-pagination)
119119
- [SFEOS Tools CLI](#sfeos-tools-cli)
120120
- [Ingesting Sample Data CLI Tool](#ingesting-sample-data-cli-tool)
121+
- [Redis for navigation](#redis-for-navigation)
121122
- [Elasticsearch Mappings](#elasticsearch-mappings)
122123
- [Managing Elasticsearch Indices](#managing-elasticsearch-indices)
123124
- [Snapshots](#snapshots)
@@ -344,6 +345,32 @@ You can customize additional settings in your `.env` file:
344345
> [!NOTE]
345346
> The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, `ES_VERIFY_CERTS` and `ES_TIMEOUT` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
346347
348+
## Redis for Navigation environment variables:
349+
These Redis configuration variables to enable proper navigation functionality in STAC FastAPI.
350+
351+
| Variable | Description | Default | Required |
352+
|-------------------------------|----------------------------------------------------------------------------------------------|--------------------------|---------------------------------------------------------------------------------------------|
353+
| `REDIS_ENABLE` | Enables or disables Redis caching for navigation. Set to `true` to use Redis, or `false` to disable. | `false` | **Required** (determines whether Redis is used at all) |
354+
| **Redis Sentinel** | | | |
355+
| `REDIS_SENTINEL_HOSTS` | Comma-separated list of Redis Sentinel hostnames/IP addresses. | `""` | Conditional (required if using Sentinel) |
356+
| `REDIS_SENTINEL_PORTS` | Comma-separated list of Redis Sentinel ports (must match order). | `"26379"` | Conditional (required if using Sentinel) |
357+
| `REDIS_SENTINEL_MASTER_NAME` | Name of the Redis master node in Sentinel configuration. | `"master"` | Conditional (required if using Sentinel) |
358+
| **Redis** | | | |
359+
| `REDIS_HOST` | Redis server hostname or IP address for Redis configuration. | `""` | Conditional (required for standalone Redis) |
360+
| `REDIS_PORT` | Redis server port for Redis configuration. | `6379` | Conditional (required for standalone Redis) |
361+
| **Both** | | | |
362+
| `REDIS_DB` | Redis database number to use for caching. | `0` (Sentinel) / `15` (Standalone) | Optional |
363+
| `REDIS_MAX_CONNECTIONS` | Maximum number of connections in the Redis connection pool. | `10` | Optional |
364+
| `REDIS_RETRY_TIMEOUT` | Enable retry on timeout for Redis operations. | `true` | Optional |
365+
| `REDIS_DECODE_RESPONSES` | Automatically decode Redis responses to strings. | `true` | Optional |
366+
| `REDIS_CLIENT_NAME` | Client name identifier for Redis connections. | `"stac-fastapi-app"` | Optional |
367+
| `REDIS_HEALTH_CHECK_INTERVAL` | Interval in seconds for Redis health checks. | `30` | Optional |
368+
| `REDIS_SELF_LINK_TTL` | Time-to-live (TTL) in seconds for storing self-links in Redis, used for pagination caching. | 1800 | Optional |
369+
370+
371+
> [!NOTE]
372+
> Use either the Sentinel configuration (`REDIS_SENTINEL_HOSTS`, `REDIS_SENTINEL_PORTS`, `REDIS_SENTINEL_MASTER_NAME`) OR the Redis configuration (`REDIS_HOST`, `REDIS_PORT`), but not both.
373+
347374
## Excluding Fields from Queryables
348375

349376
You can exclude specific fields from being exposed in the queryables endpoint and from filtering by setting the `EXCLUDED_FROM_QUERYABLES` environment variable. This is useful for hiding sensitive or internal fields that should not be queryable by API users.
@@ -615,6 +642,19 @@ The system uses a precise naming convention:
615642
python3 data_loader.py --base-url http://localhost:8080 --use-bulk
616643
```
617644

645+
## Redis for Navigation
646+
647+
The Redis cache stores navigation state for paginated results, allowing the system to maintain previous page links using tokens. The configuration supports both Redis Sentinel and standalone Redis setups.
648+
649+
Steps to configure:
650+
1. Ensure that a Redis instance is available, either a standalone server or a Sentinel-managed cluster.
651+
2. Establish a connection between STAC FastAPI and Redis instance by setting the appropriate [**environment variables**](#redis-for-navigation-environment-variables). These define the Redis host, port, authentication, and optional Sentinel settings.
652+
3. Control whether Redis caching is activated using the `REDIS_ENABLE` environment variable to `True` or `False`.
653+
4. Ensure the appropriate version of `Redis` is installed:
654+
```
655+
pip install stac-fastapi-elasticsearch[redis]
656+
```
657+
618658
## Elasticsearch Mappings
619659

620660
- **Overview**: Mappings apply to search index, not source data. They define how documents and their fields are stored and indexed.

compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ services:
2323
- BACKEND=elasticsearch
2424
- DATABASE_REFRESH=true
2525
- ENABLE_COLLECTIONS_SEARCH_ROUTE=true
26+
- REDIS_ENABLE=true
27+
- REDIS_HOST=redis
28+
- REDIS_PORT=6379
2629
ports:
2730
- "8080:8080"
2831
volumes:
@@ -31,6 +34,7 @@ services:
3134
- ./esdata:/usr/share/elasticsearch/data
3235
depends_on:
3336
- elasticsearch
37+
- redis
3438
command:
3539
bash -c "./scripts/wait-for-it-es.sh es-container:9200 && python -m stac_fastapi.elasticsearch.app"
3640

@@ -58,6 +62,9 @@ services:
5862
- BACKEND=opensearch
5963
- STAC_FASTAPI_RATE_LIMIT=200/minute
6064
- ENABLE_COLLECTIONS_SEARCH_ROUTE=true
65+
- REDIS_ENABLE=true
66+
- REDIS_HOST=redis
67+
- REDIS_PORT=6379
6168
ports:
6269
- "8082:8082"
6370
volumes:
@@ -66,6 +73,7 @@ services:
6673
- ./osdata:/usr/share/opensearch/data
6774
depends_on:
6875
- opensearch
76+
- redis
6977
command:
7078
bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app"
7179

@@ -96,3 +104,14 @@ services:
96104
- ./opensearch/snapshots:/usr/share/opensearch/snapshots
97105
ports:
98106
- "9202:9202"
107+
108+
redis:
109+
image: redis:7-alpine
110+
hostname: redis
111+
ports:
112+
- "6379:6379"
113+
volumes:
114+
- redis_test_data:/data
115+
command: redis-server
116+
volumes:
117+
redis_test_data:

stac_fastapi/core/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies = [
4444
"pygeofilter~=0.3.1",
4545
"jsonschema~=4.0.0",
4646
"slowapi~=0.1.9",
47+
"redis==6.4.0",
4748
]
4849

4950
[project.urls]

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
from stac_fastapi.core.base_settings import ApiBaseSettings
2525
from stac_fastapi.core.datetime_utils import format_datetime_range
2626
from stac_fastapi.core.models.links import PagingLinks
27+
from stac_fastapi.core.redis_utils import redis_pagination_links
2728
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
2829
from stac_fastapi.core.session import Session
29-
from stac_fastapi.core.utilities import filter_fields
30+
from stac_fastapi.core.utilities import filter_fields, get_bool_env
3031
from stac_fastapi.extensions.core.transaction import AsyncBaseTransactionsClient
3132
from stac_fastapi.extensions.core.transaction.request import (
3233
PartialCollection,
@@ -262,6 +263,7 @@ async def all_collections(
262263
A Collections object containing all the collections in the database and links to various resources.
263264
"""
264265
base_url = str(request.base_url)
266+
redis_enable = get_bool_env("REDIS_ENABLE", default=False)
265267

266268
global_max_limit = (
267269
int(os.getenv("STAC_GLOBAL_COLLECTION_MAX_LIMIT"))
@@ -417,6 +419,14 @@ async def all_collections(
417419
},
418420
]
419421

422+
if redis_enable:
423+
await redis_pagination_links(
424+
current_url=str(request.url),
425+
token=token,
426+
next_token=next_token,
427+
links=links,
428+
)
429+
420430
if next_token:
421431
next_link = PagingLinks(next=next_token, request=request).link_next()
422432
links.append(next_link)
@@ -761,8 +771,8 @@ async def post_search(
761771
search_request.limit = limit
762772

763773
base_url = str(request.base_url)
764-
765774
search = self.database.make_search()
775+
redis_enable = get_bool_env("REDIS_ENABLE", default=False)
766776

767777
if search_request.ids:
768778
search = self.database.apply_ids_filter(
@@ -866,6 +876,34 @@ async def post_search(
866876
]
867877
links = await PagingLinks(request=request, next=next_token).get_links()
868878

879+
collection_links = []
880+
# Add "collection" and "parent" rels only for /collections/{collection_id}/items
881+
if search_request.collections and "/items" in str(request.url):
882+
for collection_id in search_request.collections:
883+
collection_links.extend(
884+
[
885+
{
886+
"rel": "collection",
887+
"type": "application/json",
888+
"href": urljoin(base_url, f"collections/{collection_id}"),
889+
},
890+
{
891+
"rel": "parent",
892+
"type": "application/json",
893+
"href": urljoin(base_url, f"collections/{collection_id}"),
894+
},
895+
]
896+
)
897+
links.extend(collection_links)
898+
899+
if redis_enable:
900+
await redis_pagination_links(
901+
current_url=str(request.url),
902+
token=token_param,
903+
next_token=next_token,
904+
links=links,
905+
)
906+
869907
return stac_types.ItemCollection(
870908
type="FeatureCollection",
871909
features=items,

0 commit comments

Comments
 (0)