Metadata-Version: 2.4
Name: splent_feature_elasticsearch
Version: 0.1.2
Requires-Python: >=3.13
Description-Content-Type: text/markdown
Requires-Dist: elasticsearch<9,>=8

# 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.

```python
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. |

```python
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.

```python
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

```bash
splent feature:test splent_feature_elasticsearch
```

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