---
title: "L1ES Plugin for Elasticsearch"
description: "Elasticsearch and OpenSearch plugin for transparent expansion of 10x-optimized events"
source: "https://github.com/log-10x/elasticsearch-plugin"
icon: "simple/elasticsearch"

---

Search and visualize [compact](https://doc.log10x.com/run/transform/#compact) events in Elasticsearch and OpenSearch with no loss of log content: compact is a reversible in-place shrink, and L1ES expands events back to their original text at query time. This [open-source plugin](https://github.com/log-10x/elasticsearch-plugin){target="_blank"} transparently expands compact events at query time, maintaining full Kibana dashboard, search, and alerting capabilities while reducing ingestion and storage.

<div style="display: flex; justify-content: center; align-items: center; gap: 12px; flex-wrap: wrap;" markdown>
[:material-github: View on GitHub](https://github.com/log-10x/elasticsearch-plugin){ .md-button target="_blank" }
</div>

---

## :material-lightbulb-outline: How It Works

L1ES is an Elasticsearch plugin that intercepts search requests and expands  before returning results. Users interact with Kibana and Elasticsearch exactly as before: searching, building dashboards, and configuring alerts on the original full log data.

Two mechanisms make this transparent:

1. **Query rewriting**, standard `match`, `match_phrase`, and `multi_match` queries are automatically converted to L1ES equivalents that search across expanded content
2. **`_source` expansion**, encoded fields in `_source` are expanded in search responses, so Kibana Discover, document views, and dashboards display the original log text

### :material-arrow-right-bold-outline: Ingestion Flow

Events are [compact](https://doc.log10x.com/run/transform/#compact) at the edge and ingested into Elasticsearch with reduced payload size:

<div style="text-align: center;">

```mermaid
graph LR
    A["<div style='font-size: 14px;'>🗜️ Compact</div><div style='font-size: 10px;'>Compact Events</div>"] --> B["<div style='font-size: 14px;'>📡 Ingest</div><div style='font-size: 10px;'>Bulk API</div>"]
    B --> C["<div style='font-size: 14px;'>📋 Templates</div><div style='font-size: 10px;'>l1es_dml Index</div>"]
    B --> D["<div style='font-size: 14px;'>💾 Index</div><div style='font-size: 10px;'>Encoded Events</div>"]

    classDef edge fill:#7c3aed88,stroke:#6d28d9,color:#ffffff,stroke-width:2px,rx:8,ry:8
    classDef ingest fill:#9333ea88,stroke:#7c3aed,color:#ffffff,stroke-width:2px,rx:8,ry:8
    classDef store fill:#2563eb88,stroke:#1d4ed8,color:#ffffff,stroke-width:2px,rx:8,ry:8

    class A edge
    class B ingest
    class C,D store
```

</div>

🗜️ **Compact**: [Receiver (Compact mode)](index.md) compacts events, extracting repetitive patterns into templates

📡 **Ingest**: Encoded events forwarded to Elasticsearch via the Bulk API with reduced payload size

📋 **Templates**: [Templates](https://doc.log10x.com/run/template/) stored in the `l1es_dml` internal index for lookup at query time

💾 **Index**: [Compact events](https://doc.log10x.com/run/transform/#compact) stored with template hash references

### :material-magnify: Search Flow

Standard Elasticsearch queries are transparently rewritten to [expand](https://doc.log10x.com/run/transform/#expand) compact events:

<div style="text-align: center;">

```mermaid
graph LR
    E["<div style='font-size: 14px;'>👤 User</div><div style='font-size: 10px;'>KQL / Query DSL</div>"] --> F["<div style='font-size: 14px;'>🔄 Rewrite</div><div style='font-size: 10px;'>Intercept Query</div>"]
    F --> G["<div style='font-size: 14px;'>🔍 Match</div><div style='font-size: 10px;'>Templates + Values</div>"]
    G --> H["<div style='font-size: 14px;'>📖 Expand</div><div style='font-size: 10px;'>Decode _source</div>"]
    H --> I["<div style='font-size: 14px;'>📊 Results</div><div style='font-size: 10px;'>Full Data</div>"]

    classDef user fill:#059669,stroke:#047857,color:#ffffff,stroke-width:2px,rx:8,ry:8
    classDef hook fill:#f59e0b,stroke:#d97706,color:#ffffff,stroke-width:2px,rx:8,ry:8
    classDef result fill:#ea580c88,stroke:#c2410c,color:#ffffff,stroke-width:2px,rx:8,ry:8

    class E user
    class F,G hook
    class H,I result
```

</div>

👤 **User**: Submits search query through Kibana, API, or any Elasticsearch client

🔄 **Rewrite**: ActionFilter intercepts the search request and converts standard queries to L1ES equivalents

🔍 **Match**: L1ES queries match search terms against template patterns and encoded values

📖 **Expand**: Fetch sub-phase decodes encoded fields in `_source` and `fields`

📊 **Results**: Full events returned with original field names and values

---

## :material-file-document-outline: Compact Documents in Elasticsearch

A compact event replaces the log message with a template reference and variable values. Here is the same event before and after optimization:

**Original event `_source`:**

```json
{
  "message": "2026-02-25T14:03:22Z INFO  [http-handler] POST /api/v2/orders completed in 42ms status=200 bytes=1583 user=acct_7291",
  "@timestamp": "2026-02-25T14:03:22.000Z",
  "kubernetes.pod_name": "order-svc-6f8b4d-xk2lp"
}
```

**Compact event `_source` (as stored in Elasticsearch):**

```json
{
  "message": "~a3f29c01,2026-02-25T14:03:22Z,/api/v2/orders,42,200,1583,acct_7291",
  "@timestamp": "2026-02-25T14:03:22.000Z",
  "kubernetes.pod_name": "order-svc-6f8b4d-xk2lp"
}
```

**Expanded event (returned by L1ES at query time):**

Identical to the original. L1ES looks up template `a3f29c01` in the `l1es_dml` index, reconstructs the full message from the template pattern and variable values, and returns it in `_source`. Kibana, dashboards, and alerts see the original text.

**What changes and what stays the same:**

| Field | Compact? | Notes |
|-------|----------|-------|
| `message` (or configured source field) | Yes | Replaced with `~hash,val1,val2,...` |
| `@timestamp` | No | Passed through unchanged |
| All other fields | No | Metadata, labels, Kubernetes fields unchanged |
| Index mappings | No | Same field types, same index patterns |

Only the field registered via `_l1es/add-dml-index` is compacted. Everything else is stored and indexed exactly as before.

Compact mode encodes the log message into the registered source field while leaving Kubernetes labels, pod names, and other metadata as normal JSON. The plugin's expansion path expects the compact line at the field root, and Kibana aggregations on metadata fields keep working against the original values. The [elasticsearch-plugin README](https://github.com/log-10x/elasticsearch-plugin#where-the-savings-come-from){target="_blank"} covers the Receiver setting and an optional knob that drops repeating low-value metadata for additional storage savings.

---

## :material-magnify-expand: Query Behavior

L1ES intercepts standard Elasticsearch queries and rewrites them to search across compact content. The following query types are transparently rewritten:

### :material-check-bold: Supported Query Types

| Query Type | Behavior |
|------------|----------|
| `match` | Rewritten to `l1es_match`, searches template patterns and variable values |
| `match_phrase` | Rewritten to `l1es_match_phrase`, phrase matching across expanded content |
| `multi_match` | Rewritten to `l1es_multi_match`, multi-field search across expanded content |
| KQL (Kibana) | KQL compiles to `match`/`match_phrase`, works transparently |

These cover the queries generated by Kibana Discover, Kibana dashboards, and most saved searches. No query changes needed.

### :material-minus-circle-outline: Not Rewritten

| Query Type | Behavior |
|------------|----------|
| `term` / `terms` | Searches the raw indexed value, matches compact form, not expanded text |
| `wildcard` / `regexp` / `fuzzy` | Operates on raw indexed tokens |
| `range` | Works on non-compacted fields (e.g., `@timestamp`), not applicable to compact text fields |
| Aggregations (`terms`, `significant_terms`) | Aggregate on raw indexed values, compact field values appear as `~hash,...` in buckets |
| `highlight` | Highlights raw indexed tokens, not expanded text |

**Practical impact:** Most Kibana usage (Discover search bar, dashboard panels, alerting rules) relies on `match` and `match_phrase` queries, which are fully supported. Direct `term` queries and aggregations on the compacted field will see the raw compact form.

**Workaround for aggregations:** Use aggregations on non-compacted fields (e.g., `kubernetes.pod_name`, `level`, `@timestamp`) which are stored unchanged. For aggregations that must operate on expanded message content, use [Retriever](https://www.log10x.com/retriever.html){target="\_blank"} to expand events into a separate index.

### :material-source-merge: `_source` and `fields`

Both `_source` and `fields` responses expand the compacted field automatically when `source_decoding_enabled` and `decoding_enabled` are `true` (both default to `true`). API consumers, Kibana document views, and CSV exports receive the original text.

---

## :material-rocket-launch-outline: Quickstart

To get searchable compact events in Elasticsearch in under 15 minutes:

??? tenx-step-deploy "Step 1: Install the Plugin"

    Install the L1ES plugin on each Elasticsearch data node:

    === ":simple-elasticsearch: Elasticsearch 8.17"

        ``` { .console .copy }
        bin/elasticsearch-plugin install file:///path/to/l1es-plugin-0.3.0.es.8.17.0.zip
        ```

        Restart Elasticsearch after installing.

    === ":material-open-source-initiative: OpenSearch 2.19"

        ``` { .console .copy }
        bin/opensearch-plugin install file:///path/to/l1es-plugin-0.3.0.os.2.19.0.zip
        ```

        Restart OpenSearch after installing.

    === ":material-docker: Docker"

        ```dockerfile
        FROM docker.elastic.co/elasticsearch/elasticsearch:8.17.0
        COPY l1es-plugin-0.3.0.es.8.17.0.zip /tmp/l1es-plugin.zip
        RUN elasticsearch-plugin install --batch file:///tmp/l1es-plugin.zip
        ```

    **Prerequisites:**

    | Requirement | Description |
    |-------------|-------------|
    | Elasticsearch 8.17.0 or OpenSearch 2.19.0 | Self-hosted deployment with plugin install access |
    | Java 17+ | Required by Elasticsearch 8.x and OpenSearch 2.x |
    | Admin access | Needed to install plugins and restart nodes |

    ??? tenx-checklist "Verify Installation"

        After restart, confirm the plugin is loaded:

        ```bash
        curl -X GET 'http://localhost:9200/_l1es'
        ```

        :material-check-circle-outline:{ .success } Expected: JSON response with plugin version and description

??? tenx-step-config "Step 2: Initialize and Register"

    Initialize the plugin's internal indices:

    ```bash
    curl -X POST 'http://localhost:9200/_l1es/setup'
    ```

    Register which index and field contain encoded data:

    ```bash
    curl -X POST 'http://localhost:9200/_l1es/add-dml-index' \
      -H 'Content-Type: application/json' \
      -d '{
        "index_name": "my-logs",
        "source": "message",
        "dest": "decoded_message"
      }'
    ```

    | Parameter | Description |
    |-----------|-------------|
    | `index_name` | Your data index containing encoded events |
    | `source` | The field containing encoded events (e.g., `message`) |
    | `dest` | Field name for expanded output (defaults to `source` if omitted) |

    Repeat `add-dml-index` for each index that contains encoded data.

??? tenx-step-link "Step 3: Configure Forwarder"

    Configure your log forwarder to send encoded events and templates to Elasticsearch.

    === ":simple-fluentbit: Fluent-bit"

        **Include 10x compact configuration:**

        ```toml title="fluent-bit.conf"
        @INCLUDE ${TENX_MODULES}/pipelines/run/modules/input/forwarder/fluentbit/conf/tenx-optimize.conf
        @INCLUDE ${TENX_MODULES}/pipelines/run/modules/input/forwarder/fluentbit/conf/tenx-unix.conf
        ```

        **Configure Elasticsearch outputs:**

        ```toml title="fluent-bit.conf"
        # ========================= TEMPLATES OUTPUT =========================
        # Routes templates to l1es_dml index for plugin template lookup
        [OUTPUT]
            Name              es
            Match             tenx-template
            Host              your-elasticsearch-host.com
            Port              9200
            Index             l1es_dml
            Type              _doc
            Suppress_Type_Name On
            TLS               On
            TLS.Verify        Off

        # ========================= ENCODED EVENTS OUTPUT ====================
        # Routes encoded log events to your target index
        [OUTPUT]
            Name              es
            Match_Regex       ^(?!tenx-template).*
            Host              your-elasticsearch-host.com
            Port              9200
            Index             my-logs
            Type              _doc
            Suppress_Type_Name On
            TLS               On
            TLS.Verify        Off
        ```

    === ":simple-fluentd: Fluentd"

        **Include 10x compact configuration:**

        ```xml title="fluentd.conf"
        @include "#{ENV['TENX_MODULES']}/pipelines/run/modules/input/forwarder/fluentd/conf/tenx-optimize-unix.conf"
        ```

        **Configure Elasticsearch outputs:**

        ```xml title="fluentd.conf"
        # ========================= TEMPLATES OUTPUT =========================
        <match tenx-template>
          @type elasticsearch
          host your-elasticsearch-host.com
          port 9200
          index_name l1es_dml
          type_name _doc
          suppress_type_name true
        </match>

        # ========================= ENCODED EVENTS OUTPUT ====================
        <match **>
          @type elasticsearch
          host your-elasticsearch-host.com
          port 9200
          index_name my-logs
          type_name _doc
          suppress_type_name true
        </match>
        ```

    === ":simple-opentelemetry: OTel Collector"

        **Configure exporters in your OTel config:**

        ```yaml title="otel-collector.yaml"
        exporters:
          elasticsearch/templates:
            endpoints: ["https://your-elasticsearch-host.com:9200"]
            logs_index: "l1es_dml"
            tls:
              insecure_skip_verify: true

          elasticsearch/encoded:
            endpoints: ["https://your-elasticsearch-host.com:9200"]
            logs_index: "my-logs"
            tls:
              insecure_skip_verify: true

        service:
          pipelines:
            logs/templates:
              receivers: [tenx_templates]
              exporters: [elasticsearch/templates]
            logs/encoded:
              receivers: [tenx_encoded]
              exporters: [elasticsearch/encoded]
        ```

??? tenx-step-dashboard "Step 4: Verify End-to-End"

    Run these queries to confirm everything is working:

    **1. Check templates are loaded:**
    ```bash
    curl -s 'http://localhost:9200/l1es_dml/_count' | python3 -m json.tool
    ```
    :material-check-circle-outline:{ .success } Expected: `count` > 0

    **2. Check encoded events are indexed:**
    ```bash
    curl -s 'http://localhost:9200/my-logs/_count' | python3 -m json.tool
    ```
    :material-check-circle-outline:{ .success } Expected: `count` > 0

    **3. Search with a standard query (transparent rewriting):**
    ```bash
    curl -X POST 'http://localhost:9200/my-logs/_search' \
      -H 'Content-Type: application/json' \
      -d '{"query":{"match":{"message":"error"}},"size":3}'
    ```
    :material-check-circle-outline:{ .success } Expected: Hits returned with expanded `_source`, original log text, not `~hash,val1,val2...`

    **4. Open Kibana Discover:**

    Navigate to Kibana, select your index pattern, and search using KQL (e.g., `message: "error"`). Results should display the full original log events.

---

## :material-clipboard-check-outline: Verification Checklist

Use this checklist to diagnose issues at each stage of the pipeline.

??? tenx-checklist "Plugin Loaded?"

    **Test:**
    ```bash
    curl -X GET 'http://localhost:9200/_l1es'
    ```

    | Result | Meaning | Action |
    |--------|---------|--------|
    | JSON with version | Plugin loaded | Proceed to setup check |
    | 400/404 error | Plugin not installed | Reinstall plugin, restart Elasticsearch |

??? tenx-checklist "Internal Indices Created?"

    **Test:**
    ```bash
    curl -s 'http://localhost:9200/_cat/indices/l1es_*?v'
    ```

    | Result | Meaning | Action |
    |--------|---------|--------|
    | `l1es_dml` and `l1es_dml_indices` listed | Setup complete | Proceed to template check |
    | No indices | Setup not run | Run `POST _l1es/setup` |

??? tenx-checklist "Templates Loaded?"

    **Test:**
    ```bash
    curl -s 'http://localhost:9200/l1es_dml/_count'
    ```

    | Result | Meaning | Action |
    |--------|---------|--------|
    | Count > 0 | Templates present | Proceed to search check |
    | Count = 0 | No templates loaded | Check forwarder config, verify templates are being sent to `l1es_dml` index |

??? tenx-checklist "Queries Returning Expanded Results?"

    **Test:**
    ```bash
    curl -X POST 'http://localhost:9200/my-logs/_search' \
      -H 'Content-Type: application/json' \
      -d '{"query":{"match":{"message":"your-search-term"}},"size":1}'
    ```

    | Result | Meaning | Action |
    |--------|---------|--------|
    | Expanded `_source` with original text | Working correctly | Done |
    | `~hash,val1,val2...` in `_source` | Source expansion not active | Check `source_decoding_enabled: true` in `l1es.yml`, verify field is registered via `add-dml-index` |
    | 0 hits | Query rewriting not matching | Check `query_rewrite_enabled: true` in `l1es.yml`, verify template hash exists in `l1es_dml` |

---

## :material-bug-outline: Troubleshooting

??? tenx-troubleshoot "Standard Queries Return 0 Hits on Encoded Data"

    **Symptom:** A `match` or `match_phrase` query on an encoded field returns no results, even though the data is indexed.

    **Common Causes:**

    | Cause | Solution |
    |-------|----------|
    | `query_rewrite_enabled` is `false` | Set to `true` in `config/l1es.yml` and restart |
    | Field not registered | Run `POST _l1es/add-dml-index` for the index and field |
    | Templates not loaded | Check `l1es_dml` index has matching template hashes |
    | Wrong index name in registration | Verify `index_name` matches your data index exactly |

??? tenx-troubleshoot "Kibana Shows Encoded Text Instead of Expanded"

    **Symptom:** Kibana Discover displays `~hash,val1,val2...` instead of the original log line.

    **Common Causes:**

    | Cause | Solution |
    |-------|----------|
    | `source_decoding_enabled` is `false` | Set to `true` in `config/l1es.yml` and restart |
    | Field not registered | Run `POST _l1es/add-dml-index` with the correct `source` field |
    | Template hash not found | Verify template exists: `GET l1es_dml/_doc/<hash-from-event>` |

??? tenx-troubleshoot "Plugin Not Loading After Install"

    **Symptom:** `GET _l1es` returns 400 or the endpoint is not found.

    **Diagnostic Steps:**

    1. Check Elasticsearch logs for plugin loading errors:
       ```bash
       grep -i "l1es\|l1x" /var/log/elasticsearch/elasticsearch.log
       ```

    2. Verify the plugin is listed:
       ```bash
       bin/elasticsearch-plugin list
       ```

    | Error | Cause | Solution |
    |-------|-------|----------|
    | `java.lang.UnsupportedClassVersionError` | Wrong Java version | L1ES requires Java 17+ |
    | `Plugin version mismatch` | ES/OS version mismatch | Use the plugin build matching your ES/OS version |
    | Plugin not in list | Install failed | Reinstall with `--batch` flag |

---

## :material-cog-outline: Configuration

The plugin reads `config/l1es.yml` from its plugin directory. Key settings:

```yaml
flags:
  enabled: true                      # Master switch
  query_rewrite_enabled: true        # Transparent rewriting of standard queries
  source_decoding_enabled: true      # Decode encoded fields in _source responses
  decoding_enabled: true             # Decode encoded fields in 'fields' responses
  match_query_enabled: true          # Enable l1es_match query type
  match_pharse_query_enabled: true   # Enable l1es_match_phrase query type
  multi_match_query_enabled: true    # Enable l1es_multi_match query type
```

| Flag | Default | Description |
|------|---------|-------------|
| `query_rewrite_enabled` | `true` | Converts standard `match`/`match_phrase`/`multi_match` to L1ES equivalents |
| `source_decoding_enabled` | `true` | Expands encoded fields in `_source` for registered indices |
| `decoding_enabled` | `true` | Expands encoded fields when requested via the `fields` parameter |

---

## :material-puzzle-outline: Components

| Component | Description |
|-----------|-------------|
| [**Query Rewriter**](https://github.com/log-10x/elasticsearch-plugin/blob/main/src/main/java/co/l1x/l1es/query/L1esQueryRewriter.java){target="\_blank"} | Recursive query tree walker that converts standard queries to L1ES equivalents |
| [**Action Filter**](https://github.com/log-10x/elasticsearch-plugin/blob/main/src/main/java/co/l1x/l1es/filter/L1esQueryRewriteFilter.java){target="\_blank"} | ES ActionFilter intercepting search requests for transparent rewriting |
| [**Fetch Sub-Phase**](https://github.com/log-10x/elasticsearch-plugin/blob/main/src/main/java/co/l1x/l1es/fetch/L1esFetchSubPhase.java){target="\_blank"} | Expands `_source` and `fields` for encoded events in search responses |
| [**Template Index**](https://github.com/log-10x/elasticsearch-plugin/blob/main/src/main/java/co/l1x/l1es/dml/DmlDB.java){target="\_blank"} | Internal `l1es_dml` index storing template patterns for lookup at query time |
| [**REST Handlers**](https://github.com/log-10x/elasticsearch-plugin/tree/main/src/main/java/org/elasticsearch/plugin/l1x/handler){target="\_blank"} | `_l1es/setup`, `_l1es/add-dml-index`, and other management endpoints |

---

## :material-server-outline: Platform Support

L1ES ships as two separate plugin builds, one for Elasticsearch, one for OpenSearch. Both are functionally identical.

| Platform | Version | Plugin Build |
|----------|---------|-------------|
| Elasticsearch | 8.17.0 | `l1es-plugin-0.3.0.es.8.17.0.zip` |
| OpenSearch | 2.19.0 | `l1es-plugin-0.3.0.os.2.19.0.zip` |

The plugin must be installed on every data node in your cluster. Coordinating-only nodes and Kibana instances do not need the plugin.

For managed services (Elastic Cloud, AWS OpenSearch Service) where custom plugins cannot be installed, use [Retriever](https://doc.log10x.com/apps/retriever/) to expand compact events from S3 before ingestion.

**Version compatibility:** Each plugin build is compiled against a specific Elasticsearch/OpenSearch version. The plugin version must match your cluster version exactly. An ES 8.17.0 plugin will not load on ES 8.16.x or 8.18.x. Check [GitHub releases](https://github.com/log-10x/elasticsearch-plugin/releases){target="\_blank"} for available builds.

### :material-history: Version History

| L1ES | Elasticsearch | OpenSearch | Java | Lucene |
|------|---------------|------------|------|--------|
| 0.3.0 | 8.17.0 | 2.19.0 | 17+ | 9.12.0 |
| 0.2.x | 7.10.0 | n/a | 11+ | 8.7.0 |

!!! note "OpenSearch Config"
    After installing L1ES on OpenSearch, verify that `config/l1es-plugin/l1es.yml` exists. If missing, copy it from `plugins/l1es-plugin/config/l1es.yml`.

---

## :material-wrench-cog-outline: Production Operations

### :material-arrow-up-bold-circle-outline: Rolling Upgrades

L1ES supports rolling upgrades without downtime. Upgrade one data node at a time:

1. **Disable shard allocation**, prevent rebalancing during the restart:
    ```bash
    curl -X PUT 'http://localhost:9200/_cluster/settings' \
      -H 'Content-Type: application/json' \
      -d '{"persistent":{"cluster.routing.allocation.enable":"primaries"}}'
    ```

2. **Stop Elasticsearch** on the target node

3. **Install the new plugin version** (remove old, install new):
    ```bash
    bin/elasticsearch-plugin remove l1es
    bin/elasticsearch-plugin install file:///path/to/l1es-plugin-<new-version>.zip
    ```

4. **Start Elasticsearch** on the node

5. **Re-enable shard allocation:**
    ```bash
    curl -X PUT 'http://localhost:9200/_cluster/settings' \
      -H 'Content-Type: application/json' \
      -d '{"persistent":{"cluster.routing.allocation.enable":"all"}}'
    ```

6. **Wait for green status** before proceeding to the next node:
    ```bash
    curl -s 'http://localhost:9200/_cluster/health?wait_for_status=green&timeout=5m'
    ```

Repeat for each data node. During the upgrade, nodes running the old plugin version continue to expand queries. Mixed-version operation is safe as long as the `l1es_dml` index schema has not changed between versions (check the release notes).

### :material-arrow-up-bold-box-outline: Upgrading Elasticsearch with L1ES Installed

When upgrading Elasticsearch itself (e.g., 8.16 → 8.17), you need a matching L1ES build for the target ES version. The plugin zip is compiled against a specific ES version and will not load on a different one.

1. **Before the upgrade:** Obtain the L1ES build matching your target ES version from [GitHub releases](https://github.com/log-10x/elasticsearch-plugin/releases){target="\_blank"}. If no build exists for your target version, contact Log10x support.

2. **Per node** (rolling, one at a time):
    - Disable shard allocation (same as rolling upgrade above)
    - Stop Elasticsearch
    - Upgrade Elasticsearch to the target version
    - Remove the old L1ES plugin: `bin/elasticsearch-plugin remove l1es`
    - Install the new L1ES build: `bin/elasticsearch-plugin install file:///path/to/l1es-plugin-<version>.es.<target-es-version>.zip`
    - Start Elasticsearch
    - Re-enable shard allocation
    - Wait for green status

3. **After the upgrade:** Verify L1ES is loaded on all nodes: `GET _l1es` should return the new version. Run a test query to confirm expansion works.

The `l1es_dml` and `l1es_dml_indices` internal indices persist across the upgrade, no need to re-run `_l1es/setup` or re-register indices.

### :material-format-list-checks: Operator Checklist

Pre-production readiness checklist for L1ES deployments:

| Category | Check | How to Verify |
|----------|-------|---------------|
| **Install** | Plugin installed on every data node | `GET _l1es` returns version on each node |
| **Install** | Plugin version matches ES/OS version exactly | Compare plugin build version to `GET /` output |
| **Setup** | Internal indices created | `GET _cat/indices/l1es_*?v` lists `l1es_dml` and `l1es_dml_indices` |
| **Setup** | Target indices registered | `GET _l1es/dml-indices` lists your data indices |
| **Forwarder** | Templates routed to `l1es_dml` | `GET l1es_dml/_count` returns > 0 |
| **Forwarder** | Compact events routed to data index | `GET my-logs/_count` returns > 0 |
| **Search** | Query rewriting active | `match` query returns expanded results |
| **Search** | `_source` expansion active | Document view in Kibana shows original text |
| **Config** | `l1es.yml` flags reviewed | All three flags default `true`, adjust if needed |
| **Ops** | Cluster health green after install | `GET _cluster/health` |
| **Ops** | Plugin appears in node info | `GET _nodes/plugins` lists `l1es` on each data node |

### :material-speedometer: Performance

The L1ES plugin adds ~1.25x overhead to search queries on compacted fields. This overhead comes from two operations:

1. **Query rewriting**, translating `match`/`match_phrase` to L1ES equivalents (per-query, microseconds)
2. **`_source` expansion**, template lookup and string substitution for each hit (per-document, sub-millisecond)

The overhead scales with result set size, not data volume. A query returning 500 hits expands 500 documents regardless of whether the index holds 1M or 1B events.

**Net effect on cluster performance:** A compact storage reduction means fewer data nodes, less SSD, and less memory required for the same retention period. The reduced shard sizes also improve baseline query performance (smaller segments to scan). For most clusters, the net result is faster searches on cheaper infrastructure.

---

## :material-api: REST API

| Endpoint | Method | Description |
|----------|--------|-------------|
| `_l1es` | GET | Plugin info (version, description) |
| `_l1es/setup` | POST | Create internal indices (`l1es_dml`, `l1es_dml_indices`) |
| `_l1es/cleanup` | POST | Remove internal indices |
| `_l1es/add-dml-index` | POST | Register an encoded field mapping for an index |
| `_l1es/remove-dml-index` | POST | Unregister an encoded field mapping |

---

<br/>:material-github: This plugin is open source. View on [GitHub](https://github.com/log-10x/elasticsearch-plugin "L1ES Elasticsearch Plugin"){target="\_blank"}.
