No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-13 19:44:24 +00:00
docker docs: each product runs its own node now, so say what the prefix is still for 2026-07-30 17:54:59 +02:00
src fix: ship the compiled translations, so a released feature is actually translated 2026-07-30 18:15:07 +02:00
.gitignore fix: ship the compiled translations, so a released feature is actually translated 2026-07-30 18:15:07 +02:00
MANIFEST.in fix: ship the compiled translations, so a released feature is actually translated 2026-07-30 18:15:07 +02:00
pyproject.toml chore: release splent-io/splent_feature_elasticsearch to v0.1.2 2026-08-13 19:44:24 +00:00
README.md feat: make this usable by more than one product, and tell its failures apart 2026-07-30 15:24:59 +02:00

splent_feature_elasticsearch

Reusable Elasticsearch infrastructure for SPLENT products (service archetype). Ships a single-node Elasticsearch 8.x container and a generic, domain-agnostic ElasticsearchService that any feature can consume through the service locator.

What it provides

  • ElasticsearchService (registered in the service locator): available(), index_name(name), ensure_index(name, mappings=None), index_document(index, id, body), bulk_index(index, documents), delete_document(index, id), refresh_index(name), search(index, query_body), delete_index(name), health().
  • Docker: docker/docker-compose.dev.yml (host port published) and docker/docker-compose.prod.yml (internal-only on splent_network), both with a healthcheck and a named elasticsearch_data volume.
  • CLI: splent feature:elasticsearch status.

One node, many products

In development the CLI starts each feature as its own compose project, named after the feature reference with no product name in it. Every product in the workspace therefore talks to the same Elasticsearch container and writes into the same data volume. Index names are a flat, node-wide namespace, so two products that both index pages would overwrite each other's documents and answer searches with each other's material.

Every index name handed to the service is prefixed with ELASTICSEARCH_INDEX_PREFIX, whose shipped value __PRODUCT__ is resolved to the product name by product:env --merge and product:build. A feature keeps using its own short name and never has to know.

es.ensure_index("pages")          # creates innosoft_app_pages
es.index_name("pages")            # "innosoft_app_pages"

splent feature:elasticsearch status prints the namespace in use, which is the quickest way to check that a product is not sharing indices with its neighbour.

An index is a cache and never the authority on visibility. This service stores and returns whatever documents it is given; whether a particular reader may see a particular hit is decided at request time by the feature that owns the material.

Graceful degradation and typed errors

Elasticsearch is a soft dependency. available() never raises, and every other operation raises ElasticsearchError, never a raw client exception. Two subclasses say what happened and what to do about it.

Exception Meaning Usual response
ElasticsearchUnavailable Disabled, or the node could not be reached. Fall back to a slower search.
ElasticsearchRequestError The node answered and refused the request (malformed query, rejected mapping, missing permission). Let it surface. Retrying will not help.
from splent_framework.services.service_locator import service_proxy
from splent_io.splent_feature_elasticsearch.services import ElasticsearchUnavailable

es = service_proxy("ElasticsearchService")

try:
    results = es.search("posts", {"query": {"match": {"title": query}}})
except ElasticsearchUnavailable:
    results = fallback_sql_search(query)

Bulk indexing

A full reindex is thousands of documents, and one HTTP round trip each turns a maintenance command into something nobody runs. bulk_index consumes any iterable lazily, sends batches through the _bulk API and reports what happened, so a single bad document does not cost you the other 9999.

report = es.bulk_index(
    "pages",
    ({"id": page.id, "body": {"title": page.title, "body": page.text}} for page in pages),
)
es.refresh_index("pages")
# {"indexed": 9999, "failed": 1, "errors": [{"id": 42, "error": "..."}]}

Only a failure of the request itself raises. Per-document failures come back in the report, with the error list capped so a broken mapping cannot flood a log.

Configuration

Env var Default Purpose
ELASTICSEARCH_URL http://localhost:9200 Node URL. The Docker .env sets http://splent_feature_elasticsearch:9200 for container-to-container access.
ELASTICSEARCH_ENABLED true Feature kill switch. When false all operations degrade immediately.
ELASTICSEARCH_INDEX_PREFIX splent Namespace for this product's indices. The Docker .env ships __PRODUCT__, resolved to the product name.
ELASTICSEARCH_HOST_PORT 9201 Host port published in development only.
ELASTICSEARCH_JAVA_OPTS -Xms512m -Xmx512m Heap for the node, read by the production compose file.

Tests

splent feature:test splent_feature_elasticsearch

Unit tests mock the client boundary, so no network access is required.