<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Francisco Fernández Castaño - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Francisco Fernández Castaño - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/francisco-fernandez-castano</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/francisco-fernandez-castano</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/francisco-fernandez-castano.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 01:48:20 GMT</lastBuildDate>
  <item>
    <title><![CDATA[How Elasticsearch cuts time-series storage by 34% with synthetic _id and bloom filters]]></title>
    <description><![CDATA[Learn how synthetic _id uses bloom filters to cut time-series storage by 34% while maintaining full API compatibility.]]></description>
    <content:encoded><![CDATA[<p>Synthetic <code>_id</code> reduces time-series index storage by up to 34% and eliminates 6% CPU overhead at ingest. Instead of building an inverted index for <code>_id</code>, Elasticsearch computes the document identifier on the fly from <code>_tsid</code> and <code>@timestamp</code>, using a bloom filter for deduplication. This optimization ships in Elasticsearch 9.4 and is already live on Elastic Cloud Serverless.</p><p>This post is a deep dive into the implementation. For context on how synthetic <code>_id</code> fits into the broader metrics performance story, see <a href="https://www.elastic.co/search-labs/blog/elasticsearch-columnar-metrics-engine-30x-faster-prometheus">How we rebuilt Elasticsearch as a leading columnar metrics datastore</a> to achieve up to 6.6x improvement in storage efficiency and 50% improvement in indexing throughput for OpenTelemetry metrics.</p><p>We'll start by explaining why the <code>_id</code> field is expensive for time-series workloads. We'll then describe how synthetic <code>_id</code> works and how it uses a bloom filter to optimize document deduplications instead of maintaining a traditional inverted index. Finally, we'll share the performance results from our benchmarks and serverless production deployments.</p><h2>The hidden cost of _id in time-series indices</h2><p>Time-series indices are a specialized index mode optimized for metrics, logs, traces, and other timestamped data. They store sequences of data points (like CPU usage, stock prices, or sensor readings) that track changes to specific entities over time. In Elasticsearch, each of these data points is indexed as a document with a unique identifier called <code>_id</code>. This identifier is used to look up, update, or delete specific documents. When a document is indexed in Elasticsearch, the system checks whether a document with the same <code>_id</code> already exists. Depending on the operation type (<code>op_type</code>), an existing document is either replaced (<code>index</code>) or the new document is rejected (<code>create</code>); the latter is the most common path for metrics ingestion.</p><p>To perform this lookup efficiently, Elasticsearch builds an <a href="https://en.wikipedia.org/wiki/Inverted_index">inverted index</a> for the <code>_id</code> field. This inverted index maps each <code>_id</code> value to its location in the index, enabling fast document lookups. Until version 8.11, the <code>_id</code> value was also stored separately in order to be returned in search results and other APIs. From 8.11 and onwards, we optimized Elasticsearch to only store this value temporarily for document replication purposes, the value being quickly merged away and reconstructed on demand.</p><p>For many use cases, building the inverted index and storing it is an acceptable overhead. But for time-series data, like metrics or traces, the cost can add up quickly. Our experiments showed that building the inverted index for the field <code>_id</code> adds 6% CPU overhead compared to indexing without it. In some extreme cases, we benchmarked that it could reduce indexing throughput by 25%.</p><p>This overhead is especially painful for time-series workloads where data points are typically small (often just a timestamp and a few numeric values) and compress extremely well. The <code>_id</code> field, however, doesn't benefit from the same compression. As a result, the inverted index for <code>_id</code> can represent a disproportionate share of the total storage. In our benchmarks with OpenTelemetry (OTel) metrics, the <code>_id</code> inverted index alone consumed around 5 bytes of the total 25 bytes per data point.</p><p>We considered several approaches to eliminate this overhead:</p><ul><li><p>Stop indexing <code>_id</code> and checking for duplicates: This would be the simplest solution, but without deduplication, duplicate data points could corrupt aggregations. A gauge average, for instance, would be skewed by repeated values.</p></li><li><p>Accept duplicates during indexing, deduplicate at query time: This preserves correctness but adds overhead to every query, degrading dashboard responsiveness.</p></li><li><p>Deduplicate during segment merges: Duplicates would eventually be removed, but queries on unmerged segments would still return results with duplicates.</p></li><li><p>Synthetic <code>_id</code>: Compute the document identifier on the fly from fields that already uniquely identify each data point, and use a lightweight bloom filter for deduplication instead of a full inverted index.</p></li></ul><p>We chose synthetic <code>_id</code> because it maintains correctness at ingest time while eliminating the storage and CPU overhead of the traditional approach. And we decided to implement it for time-series indices because they’re very well suited for this optimization.</p><p>In time-series indices, the <code>_id</code> isn’t arbitrary. Each document has a <strong>time series identifier</strong> (<code>_tsid</code>) and a <strong>timestamp</strong> (<code>@timestamp</code>). The <code>_tsid</code> is generated from the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-dimension">dimensions fields</a> of the document (like <code>host.name</code>, <code>pod.name</code>, or <code>sensor_id</code>), while the <code>@timestamp</code> marks the point in time of the document. Together, these two fields uniquely identify the document: There can only be one data point for a given time series at a given moment in time. This means we can derive the <code>_id</code> from the <code>_tsid</code> and <code>@timestamp</code> field values, rather than storing it separately.</p><h2>How does synthetic _id work in Elasticsearch?</h2><p>With synthetic <code>_id</code>, Elasticsearch computes the document identifier on the fly as the combination of the <code>_tsid</code> and <code>@timestamp</code> fields. This computed value is used wherever <code>_id</code> would normally be used: in API responses, for document lookups, and for deduplication. However, it’s never stored in an inverted index nor is it stored on disk for later retrieval.</p><p>The challenge is deduplication. When a new document arrives, Elasticsearch must verify that no document with the same <code>_id</code> already exists. Without an inverted index on <code>_id</code>, how can we perform this check efficiently?</p><h3>How synthetic _id simulates an inverted index without building one</h3><p>Our Elastic Lucene experts suggested a clever idea: Since <code>_tsid</code> and <code>@timestamp</code> are already stored as doc values, we could expose our own custom Lucene postings format that simulates an inverted index without actually building one.</p><p>This means that when Elasticsearch needs to look up a document by its <code>_id</code>, it uses the same code path as usual: It queries the underlying Lucene index to look up the <code>_id</code> term. But instead of hitting a real inverted index, our custom postings format intercepts the call, extracts the <code>_tsid</code> and <code>@timestamp</code> encoded in the synthetic <code>_id</code>, and uses their doc values to locate the document. Because time-series indices are sorted by these fields, documents belonging to the same time series are stored contiguously. This allows Elasticsearch to skip large subsets of nonmatching documents (sometimes entire segments) to find the target document(s) quickly.</p><p>While this process is efficient, it can involve several random-access reads: looking up the <code>_tsid</code> value, scanning for matching documents, and reading timestamps. For the common case in time-series indices where we don’t expect the document to already exist, we wanted to fail fast without touching doc values at all.</p><h3>Bloom filters for fast membership testing</h3><p>We solve this problem using a <a href="https://en.wikipedia.org/wiki/Bloom_filter"><strong>bloom filter</strong></a>, a probabilistic data structure that can quickly answer the question <em>Could this element be in the set?</em> with a small risk of false positives but no risk of false negatives. In other words, a bloom filter might occasionally say <em>yes</em> when the answer is actually <em>no</em>, but it will never say <em>no</em> when the answer is <em>yes</em>.</p><p>When a document is indexed, its synthetic <code>_id</code> is added to the bloom filter. When a new document arrives, we first check the bloom filter. If the bloom filter says <em>no</em>, we know for certain that no document with this <code>_id</code> exists and we can proceed with indexing immediately. If the bloom filter says <em>maybe yes</em>, we fall back to the more expensive verification using the <code>_tsid</code> and <code>@timestamp</code> doc values.</p><h3>Synthetic _id indexing workflow: step by step</h3><p>Let's walk through what happens when a document is indexed into a time-series index with synthetic <code>_id</code> enabled:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt387a8573301f120e/6a3e41e975bd4076e6a77b5d/61ef279f09c5447009d3695f154129fba6fe510d-1048x1462.png" alt="Flowchart on a dark background showing document indexing steps using synthetic IDs, bloom filters, and duplicate handling paths." /><ol><li><p><strong>Compute the synthetic </strong><strong><code>_id</code></strong>: Elasticsearch calculates <code>_id</code> as a combination of <code>_tsid || @timestamp</code>.</p></li><li><p><strong>Check the live version map</strong>: Like today, we first check an in-memory map of recently indexed documents. If the document is present in this map, we can handle the duplicate immediately.</p></li><li><p><strong>Filter segments by timestamp</strong>: Time-series indices are sorted by <code>_tsid</code> and <code>@timestamp</code>. We can skip any segment whose timestamp range does not overlap with the incoming document's timestamp.</p></li><li><p><strong>Check the bloom filter</strong>: For each candidate segment, we test whether the <code>_id</code> might exist using the bloom filter.</p></li><li><p><strong>Verify if needed</strong>: If the bloom filter returns a positive result, we look up the document using the <code>_tsid</code> and <code>@timestamp</code> doc values. Since documents are sorted by these fields, this lookup is efficient.</p></li><li><p><strong>Index the document</strong>: If no existing version is found, the document is indexed. The <code>_id</code> is added to the segment's bloom filter, but no inverted index is built and the field value is never stored.</p></li></ol><p>In the common case where new data arrives with recent timestamps, step 3 eliminates most segments from consideration, and step 4 quickly confirms that the document is new. The expensive verification in step 5 only happens on bloom filter false positives, which are expected to be rare.</p><h3>Bloom filter false positive rate: how Elasticsearch keeps it low</h3><p>One challenge with bloom-filter-based deduplication is controlling the false positive rate without sacrificing the storage efficiency we were after. To size bloom filters effectively, we consider the number of data points in each segment and target both a low false positive rate and a bit set saturation below 50%.</p><p>The saturation target serves a specific purpose: When segments are merged, we OR the bit sets rather than rebuilding bloom filters from scratch. This makes merges fast but means the false positive rate converges toward 100% as segments are merged repeatedly. Keeping saturation below 50% before merging buys headroom, delaying that convergence.</p><p>The low false positive rate target is justified by access patterns: Recent segments are checked far more often than older ones, since we prune the search space based on data point timestamps. Older, heavily merged segments with degraded bloom filters are unlikely to be checked.</p><h2>Synthetic _id performance benchmarks: indexing and storage</h2><p>We ran extensive benchmarks to validate our implementation.</p><h3>Indexing throughput</h3><p>A core goal of this effort was to match or improve on existing indexing throughput. In principle, the new approach does less work: Building an inverted index for <code>_id</code> requires hashing each value, building and maintaining complex data structures in memory, and flushing them to disk. These structures must also be reconstructed during segment merges, adding CPU and I/O overhead in high-throughput use cases.</p><p>Building a bloom filter isn't free (we still hash each value), but the memory footprint is smaller and there are no complex data structures to maintain or flush. The bloom filter is also cheap to merge: When possible, we simply OR the bit sets together rather than rebuilding from scratch.</p><p>The main cost of synthetic <code>_id</code> comes from verifying potential duplicates using doc values. However, this cost is mitigated by two factors: First, bloom filter false positives are rare, so most documents skip this step entirely. Second, time-series indices are sorted by <code>_tsid</code> and <code>@timestamp</code>, which means doc value lookups can skip large blocks of nonmatching documents efficiently.</p><p>In practice, that's exactly what we observed. Even accounting for the extra seeks needed to verify matches against the tsid and timestamp when a bloom filter returns a positive, throughput came out comparable or better than before. The savings from not building and merging the inverted index outweigh the occasional cost of a false positive check, as confirmed by our <a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/f7e091a0-1db1-11ed-920a-3b1141502d24?_g=(refreshInterval:(pause:!t,value:60000),time:(from:'2026-03-16T00:00:00.000Z',to:'2026-03-19T23:30:00.000Z'))&amp;_a=(viewMode:view)">nightly benchmarks</a>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte73bd65d8545658d/6a3e41eca48ab9170737c817/32b5b068ef6dd7cd512f12dc9d50d8769f2930d4-1999x509.png" alt="Line graph titled “nightly‑tsdb‑indexing‑throughput,” showing nightly benchmark results for document indexing rates in docs per second over three days, with four colored lines representing different indexing operations." /><h3>Storage savings</h3><p>In our benchmarks with OTel metrics, synthetic <code>_id</code> reduced storage by approximately 5 bytes per data point. For a dataset where documents average 25 bytes per data point, this represents a 20% reduction in storage from this single optimization alone.</p><p>These results were soon confirmed by our <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/tsdb/nightly/default/90d">nightly benchmarks</a>.The chart below shows the storage footprint reduction over time as we enabled the synthetic <code>_id</code> feature on March 19, 2026:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3cb1cf931ecebba9/6a3e41efb8c8ed6dec833e9d/0c0ac1f2c9716a6f99b6e055535db0def77bf930-1898x956.png" alt="Line graph titled “Disk usage” showing TSDB and downsampling data from March 15 to March 23, 2026, with disk usage measured in gigabytes per 24 hours. A teal line for TSDB drops near March 19 and stabilizes around 1.9 GB, while a brown line for downsampling decreases to 2.3 GB after the same date." /><p>Our standard time series database (TSDB) benchmark showed a reduction from 2.5 GiB to 1.9 GiB (24%). Similarly the time-series downsampling benchmark showed a comparable reduction from 3 GiB to 2.3 GiB (23%).</p><p>Another benchmark, more focused on metrics, <a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/37270832-cd2d-4ea7-8222-e61e8ad742a3?_g=(refreshInterval:(pause:!t,value:60000),time:(from:'2026-03-16T00:00:00.000Z',to:'2026-03-19T23:30:00.000Z'))&amp;_a=(viewMode:view)">showed an even better reduction</a>, from 3.0 GiB to 2.0 GiB (34%):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd66d7617467e01c0/6a3e41f2809064d8e0e00d7f/2a243db0f3ad95e761fb334ffd6fea47f7787eee-1999x447.png" alt="Line graph titled “Dataset size,” showing two turquoise lines that decline from March 16 to 19, 2026, each representing a different TSDB metric. The x‑axis marks daily timestamps, and the y‑axis shows dataset size decreasing." /><h2>API compatibility</h2><p>An important design goal was maintaining compatibility with existing Elasticsearch APIs. With synthetic <code>_id</code>, all document APIs continue to work as expected: Bulk, Get, Update, Delete, Reindex, and Update/Delete by Query. This compatibility layer also limited the blast radius of the change, ensuring any issues would be contained to the internal implementation.</p><p>When the <code>_id</code> isn’t provided in an API request, Elasticsearch computes it from the <code>_tsid</code> and <code>@timestamp</code> fields. To check if the document already exists, it first queries the bloom filter and, if needed, falls back to doc values. The <code>_id</code> is also synthesized on demand from doc values when returning documents in search results or API responses.</p><p>One case that requires special handling is searching or filtering by <code>_id</code> prefix or pattern. Such queries require scanning many documents to find matching documents, and while this works correctly, it incurs a performance penalty compared to a direct <code>_id</code> lookup. We don’t expect this use case to be common for time-series indices though.</p><h2>Elasticsearch 9.4 and Elastic Cloud Serverless availability</h2><p>The synthetic <code>_id</code> feature will be released in Elasticsearch 9.4.0 and is already available on <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>.</p><p>No configuration is required: The feature is enabled by default, and newly created time-series indices (including those created on datastream rollover) will automatically benefit from this optimization. Existing time-series indices created before 9.4 will continue to create inverted indices for the <code>_id</code> field.</p><p>We expect synthetic <code>_id</code> to perform well across all time-series use cases. However, in some very specific, update-heavy use cases, if you encounter performance issues, the feature can be disabled by setting <code>index.mapping.synthetic_id</code> to <code>false</code> for new indices.</p><h2>Summary: synthetic _id storage and performance gains</h2><p>In this article, we’ve presented how synthetic <code>_id</code> eliminates the storage and compute overhead of document identifiers in time-series indices. By computing <code>_id</code> on the fly from <code>_tsid</code> and <code>@timestamp</code>, and using a bloom filter for deduplication, we achieve comparable or better indexing performance with up to 34% reduction in storage footprint while maintaining full API compatibility. For users running large-scale time-series workloads, this translates directly into lower infrastructure costs.</p><h2>Roadmap: what comes after synthetic _id</h2><p>Synthetic <code>_id</code> is part of a broader effort to reduce storage overhead in Elasticsearch.</p><ul><li><p><strong>Sequence number trimming:</strong> Every document carries a sequence number for replication and concurrency control. For append-only time-series data, these become redundant after segments are merged. Elasticsearch 9.4 now trims them during merges to reclaim even more storage: We'll cover this optimization in detail in an upcoming blog post.</p></li><li><p><strong>Synthetic _id beyond time-series:</strong> We’re exploring how to bring synthetic <code>_id</code> to regular indices by letting users declare which fields uniquely identify their documents and configuring index sorting on those fields to enable efficient lookups.</p></li></ul><p>Stay tuned!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-synthetic-id-time-series-storage</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-synthetic-id-time-series-storage</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Tanguy Leroux,Francisco Fernández Castaño,Anton Persson]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbe7de1872f1527e3/6a17de303e9e452974ba1374/a70c5403064d5bbceff66a17373332362227f13c-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How we optimized refresh costs in Elasticsearch Serverless]]></title>
    <description><![CDATA[We explore how serverless Elasticsearch facilitates searches using data stored in a blob store while maintaining the same visibility semantics as stateful Elasticsearch. We discuss the challenges encountered during implementation and share strategies for balancing costs and complexity.]]></description>
    <content:encoded><![CDATA[<p>Recently, we’ve <a href="https://www.elastic.co/blog/search-ai-lake-elastic-cloud-serverless">launched</a> the Elastic Cloud Serverless offering that aims to provide a seamless experience to run search workloads in the cloud. To launch this, we’ve rearchitected Elasticsearch to <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">decouple</a> storage from compute, where data is stored in a cloud blob store that provides virtually infinite storage and scalability. In this blog post, we’ll dive into how we removed a strong relationship between the number of indices and the number of object store calls, allowing us to improve UX and reduce costs at the same time.</p><p>Before we dive into the changes we made, it’s essential to first understand the interplay between Elasticsearch and Lucene.</p><p>Elasticsearch uses Lucene, a high-performance, open-source library written in Java, for full text indexing and search. When a document is indexed into Elasticsearch, it isn't immediately written to disk by Lucene. Instead, Lucene updates its internal in-memory data structures. Once enough data accumulates or a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html">refresh</a> is triggered, these documents are then written to disk, creating a new set of immutable files known as segments in Lucene terminology. The indexed documents are not available for search until the segments are written to disk. That’s the reason why <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html">refresh</a> is such an important concept in Elasticsearch. You might be wondering how durability is ensured when documents are kept in memory until a refresh is triggered. This is achieved through the Translog, which stores durably every operation to guarantee data persistence and recovery in case of failure.</p><p>Now that we know what Lucene segments are and why refreshes are needed in Elasticsearch, we can explore how refresh behavior differs between stateful Elasticsearch and <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">serverless Elasticsearch</a>.</p><h2>Refreshes in stateful Elasticsearch</h2><p>In Elasticsearch, indices are divided into multiple shards, each consisting of a primary shard and potentially multiple replica shards. In stateful Elasticsearch, when a document is indexed, it is first routed to the primary shard, where Lucene processes and indexes it. After indexing on the primary shard, the document is then routed to the replica shards, where it is indexed by these copies.</p><p>As mentioned earlier, a refresh is needed to make these indexed documents searchable. In stateful Elasticsearch, a refresh writes the Lucene in-memory data structures to disk without performing an fsync. Refreshes are scheduled periodically, with each node executing them at different times. This process will create distinct Lucene segment files on each node, all containing the same set of documents.</p><h2>Refreshes in serverless Elasticsearch</h2><p>In contrast, serverless Elasticsearch employs a segment-based replication model. In this approach, one node per shard handles document indexing and generates Lucene segments. These segments are uploaded to the blob store once a refresh is initiated. Subsequently, search nodes are informed about these new Lucene segments, which they can read directly from the blob store.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7cc4aefbc63dca3c/6a17d7783e03d793524f2ac9/efc718c485d64c1911365a7e20a9435aeb1fbdc0-1440x754.png" alt="Refresh flow before optimizations in Elasticsearch" /><p>The illustration above demonstrates how a refresh works in serverless Elasticsearch:</p><ol><li><p>The indexing node, where all the documents were indexed, receives the refresh request and Lucene writes the in-memory data structures to disk, similar to how a stateful refresh operates.</p></li><li><p>The segment files are uploaded to the blob store as a single file (known as a stateless compound commit). In the illustration, S4 is uploaded.</p></li><li><p>Once the segment files are uploaded to the blob store, the indexing node sends a message to each search node, notifying them of the new segment files so they can perform searches on the newly indexed documents.</p></li><li><p>The search nodes fetch the necessary data from the blob store when executing searches.</p></li></ol><p>This model offers the advantage of lightweight nodes, as data is stored in the blob store. This makes scaling or reallocating workloads between nodes more cost-effective compared to stateful Elasticsearch, where data must be transferred to the new node containing the new shard.</p><p>One aspect worth considering is the additional object store request costs associated with each refresh in serverless Elasticsearch. Every refresh operation created a new object in the object store, resulting in an object store PUT request that incurs associated costs. This led to a linear relationship between the number of indices and the number of object store PUT requests. With enough refreshes, object store costs could surpass the cost of the hardware itself. To address this, we initially implemented refresh throttling measures to manage costs effectively and mitigate potential issues over time. This blog post describes the next step in that effort, which allowed us to refresh faster and at a manageable cost.</p><h2>Refresh cost optimizations in serverless Elasticsearch</h2><p>As previously mentioned, the serverless Elasticsearch architecture provides numerous benefits. However, to manage refresh costs effectively, we made decisions that occasionally impacted user experience. One such decision was enforcing a default refresh interval of 15 seconds, meaning that in some cases, newly indexed data won't become searchable until 15 seconds have passed. Despite our efforts, scenarios arose where object store expenses became prohibitive, prompting us to reassess our approach. In this section, we will delve into how we successfully decoupled refresh operations from object store calls to address these challenges without compromising user experience.</p><p>After evaluating various solutions—from temporary storage of segments in distributed file systems like NFS to direct pushing of segments into search nodes—we settled on an approach relying on serving segment data from indexing nodes directly to search nodes.</p><p>Rather than letting refresh immediately upload new Lucene segments to the blob store, index nodes now accumulate segments from refreshes and upload them as a single blob later. This enables index nodes to serve reads from search nodes in a manner akin to a blob store, delaying segment uploads until sufficient data accumulates or a predetermined time interval elapses.</p><p>This strategy grants us complete control over the size of the blobs uploaded to the blob store, enabling us to determine when request costs become negligible in comparison to hardware costs.</p><h3>Batched compound commits</h3><p>We aimed to implement this enhancement incrementally and ensure backward compatibility with existing data stored in the blob store. Therefore, we opted to maintain the same file format for storing Lucene segments in the blob store. For context, Lucene segments comprise multiple files, each serving a distinct role. To streamline the upload process and minimize PUT requests, we introduced compound commits: single blobs containing all segment files consecutively, accompanied by a metadata header, including a directory of the files in the compound commit.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7207e2d6008df66e/6a17d779ec0f8984445a6446/a503ec1c16498a4ad86de3c1fce4d13e472144da-1440x441.png" alt="Compound commit file format" /><p>When retrieving a compound commit from the blob store, such as during shard relocation, our primary focus is typically on the compound commit header. This header is crucial as it contains the essential data needed to promptly populate internal data structures. With this in mind, we realized we could maintain the existing file format but streamline it so that each blob would sequentially append one compound commit after another. We denominated this new file format, batched compound commit.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt321e94e2043ef619/6a17d77aec0f8907455a644a/3c264c4b43632f508c96a6ebd2c381402a83fd5f-1440x374.png" alt="Batched compound commit file format" /><p>Since each compound commit's size is stored in its header, retrieving the headers of all compound commits within a batched compound commit is straightforward; we can sequentially read each header by simply seeking the next entry. When handling blobs in the old format, they are treated as singleton batched compound commits. Another critical aspect of our file format is maintaining fixed offsets for each Lucene segment file once it's appended into a batched compound commit. This ensures consistency whether the file is served from the index node or the blob store. It also prevents the need to evict cached entries on search nodes when the batched compound commit is eventually uploaded to the blob store.</p><h3>New refresh lifecycle</h3><p>Index nodes will now accumulate Lucene segments from refreshes until enough data is gathered to upload them as a single blob. Let us explore how index and search nodes coordinate to determine where to access this data from.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta3c527801c519b6d/6a17d77ce9ea8761a5a9c41d/578e9dbfeb31673e86429820479c211db5a27434-1440x802.png" alt="Optimized refresh flow in Elasticsearch serverless" /><p>As shown in the illustration above, the following steps occur during the optimized refresh process in serverless Elasticsearch:</p><ol><li><p>The index node receives a refresh request, writes a new set of Lucene segments to its local disk, and adds these segments to the pending batched compound commit for eventual upload.</p></li><li><p>The index node notifies the search node about these new segments, providing details about the involved segments and their locations (blob store or index node).</p></li><li><p>When a search node needs a segment to fulfill a query, it decides whether to get it from the blob store or the index node and caches the data locally.</p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt782c93954782c163/6a17d77d4b055d0b68432050/8e051b6251fe96510dd1d31f679c54c042cd95e5-1440x721.png" alt="Optimized refresh upload flow in Elasticsearch serverless" /><p>The image above illustrates the process of uploading data to the blob store in serverless Elasticsearch once enough segments have accumulated in the indexing nodes or after a specified amount of time has elapsed.</p><ol><li><p>A refresh adds a new segment to the batched compound commit and the accumulated data reaches 16 MB, or a certain amount of time has passed since the last refresh, from this point onwards new segments are accumulated into a new batched compound commit.</p></li><li><p>The indexing node begins uploading the accumulated segments as a single blob to the object store.</p></li><li><p>The indexing node notifies the search node replicas of the latest segment uploaded to the object store, instructing them to fetch data from these segments from the blob store going forward.</p></li><li><p>If a search requires data that isn't cached locally, it will retrieve the necessary information from the blob store, while any previously fetched data from the indexing node remains valid even after the upload.</p></li></ol><h3>Considerations and tradeoffs</h3><p>The approach chosen blurs the clear separation between storage and compute, requiring index nodes to handle storage requests until Lucene segments are eventually uploaded to the blob store. However, the overhead from these storage requests is minimal and we have not observed impact on indexing throughput.</p><p>We'll note that we keep translog entries until corresponding data has been uploaded to the blob store, hence the approach maintains existing data safety guarantees. Recovery times after a crash may be slightly longer, but we consider this an acceptable trade-off.</p><h2>Conclusions</h2><p>This blog post has explored our transition towards a more cloud-native approach, emphasizing its many benefits alongside the critical cost consideration. We traced our evolution from a model where each new Lucene segment generated a distinct object in the object store. This led to cost and user experience challenges in specific serverless workloads compared to stateful Elasticsearch. Batching object store uploads enabled us to minimize the number of object store requests and enhance the cost efficiency of our serverless offering.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6478e2975a6ccedb/6a17d77f3e03d7925a4f2acd/58096951dd3f12d0b60072867febc6b7c90ecc26-1440x673.png" alt="Reduction in PUT requests" /><h2>Acknowledgments</h2><p>We would like to acknowledge the contributions of Iraklis Psaroudakis, Tanguy Leroux, and Yang Wang. Their efforts were instrumental in the success of this project.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-refresh-costs-serverless</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-refresh-costs-serverless</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <dc:creator><![CDATA[Francisco Fernández Castaño,Henning Andersen]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt88526af16bafdb7c/6a17d7807f6f15825dc0998d/d11e1ba058784ec92b8953fb8db62e1bad21c210-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 31 Jul 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Ingest autoscaling in Elasticsearch]]></title>
    <description><![CDATA[Learn more about how Elasticsearch autoscales to address ingestion load.]]></description>
    <content:encoded><![CDATA[<p>Sizing an Elasticsearch cluster correctly is not easy. The optimal size of the cluster depends on the workload that the cluster is experiencing, which may change over time. Autoscaling adapts the cluster size to the workload automatically without human intervention. It avoids over-provisioning resources for the cluster only to accommodate peak usage and it also prevents degrading cluster performance in case of under-provisioning.</p><p>We rely on this mechanism to free users of our <a href="https://www.elastic.co/docs/current/serverless">Elastic Cloud Serverless</a> offering from having to make sizing decisions for the <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">indexing tier</a>. Ingest autoscaling requires continuously estimating the resources required to handle the incoming workload, and provisioning and de-provisioning these resources in a timely manner.</p><p>In this blog post we explore ingest autoscaling in Elasticsearch, covering the following:</p><ul><li><p>How ingest autoscaling works in Elasticsearch</p></li><li><p>Which metrics we use to quantify the indexing workload the cluster experiences in order to estimate resources required to handle that workload</p></li><li><p>How these metrics drive the autoscaling decisions.</p></li></ul><h2>Ingest autoscaling overview</h2><p>Ingest autoscaling in Elasticsearch is driven by a set of metrics that is exposed by Elasticsearch itself. These metrics reflect the ingestion load and the memory requirement of the indexing tier. Elasticsearch provides an autoscaling metrics API that serves these metrics which allows an external component to monitor these metrics and make decisions whether the cluster size needs to change (see Figure 1).</p><p>In the Elastic Cloud Serverless service, there is an autoscaler component which is a Kubernetes Controller. The autoscaler polls the Elasticsearch autoscaling metrics API periodically and calculates the desired cluster size based on these metrics. If the desired cluster size is different from the current one, the autoscaler changes the cluster size to consolidate the available resources in the cluster towards the desired resources. This change is both in terms of the number of Elasticsearch nodes in the cluster and the CPU, memory and disk available to each node.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltce3b99ecd11442dd/6a17d7126df73179750a0d41/7c0cb0b822ca2a6531d35df15a0226a51d18a1cb-1440x1119.png" alt="overview" /><p><strong>Figure 1</strong>: ingestion autoscaling overview</p><p></p><p>An important consideration for ingest autoscaling is that when the cluster receives a spike in the indexing load the autoscaling process can take some time until it effectively adapts the cluster size. While we try to keep this reaction time as low as possible, it cannot be instantaneous. Therefore, while the cluster is scaling up, the Elasticsearch cluster should be able to temporarily push back on the load it receives if the increased load is otherwise going to cause cluster instability issues. The increase in the indexing load can manifest itself in the cluster requiring more resources, i.e., CPU, memory or disk. Elasticsearch has protection mechanisms that allows nodes to push back on the indexing load if any of these resources becomes a bottleneck.</p><p>To handle indexing requests Elasticsearch uses dedicated thread pools sized based on the number of cores available to the node. If the increased indexing load results in CPU or other resources becoming a bottleneck, incoming indexing requests are queued. <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-threadpool.html">The maximum size of this queue</a> is limited and any request arriving at the node when the queue is full will be rejected with a 429 HTTP code.</p><p>Elasticsearch also keeps track of the required memory to address ongoing indexing requests and rejects incoming requests (with a 429) if the indexing buffer <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-indexing-pressure.html">grows beyond 10% of the available heap memory</a>. This limits the memory used for indexing and ensures the node will not go out of memory.</p><p>The Elastic Cloud Serverless offering relies on the object store as the main storage for indexed data. The local disk on the nodes are used temporarily to hold indexed data. Periodically, Elasticsearch uploads the indexed data to the object store which allows freeing up the local disk space as we rely on the object store for durability of the indexed document. Nonetheless, under high indexing load, it is possible for the node to run out of disk space before the periodic upload task gets a chance to run and free up the local disk space. To handle these cases, Elasticsearch monitors the available local disk space and if necessary throttles the indexing activity while it attempts to free up space by enforcing an upload to the object store rather than waiting for the periodic upload to take place. Note that this throttling in turn results in queueing of the incoming indexing requests.</p><p>These protection mechanisms allow an Elasticsearch cluster to temporarily reject requests and provide the client with a response that indicates that the cluster is overloaded while the cluster tries to scale up. This push-back signal from Elasticsearch provides the client with a chance to react by reducing the load if possible or retrying the request which should eventually succeed if retried when the cluster is scaled up.</p><h2>Metrics</h2><p>The two metrics that are used for ingest autoscaling in Elasticsearch are ingestion load and memory.</p><h3>Ingestion load</h3><p>Ingestion load represents the number of threads that is needed to cope with the current indexing load. The autoscaling metrics API exposes a list of ingestion load values, one for each indexing node. Note that as the write thread pools (which handle indexing requests) are sized based on the number of CPU cores on the node, this essentially determines the total number of cores that is needed in the cluster to handle the indexing workload.</p><p>The ingestion load on each indexing node consists of two components:</p><ul><li><p>Thread pool utilization: the average number of threads in the write thread pool processing indexing requests during that sampling period.</p></li><li><p>Queued ingestion load: the estimated number of threads needed to handle queued write requests.</p></li></ul><p>The ingestion load of each indexing node is calculated as the sum of these two values for <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-threadpool.html">all the three write thread pools</a>. The total ingestion load of the Elasticsearch cluster is the sum of the ingestion load of the individual nodes.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4e0ba9f4e7ae59b9/6a17d71463baff6110741ab5/b1ab391f2639aaa4b7807a37a23fb9c5375f9690-1440x831.png" alt="Figure 2: ingestion load components" /><p><strong>Figure 2</strong>: ingestion load components</p><p></p><p>The thread pool utilization is an <a href="https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">exponentially weighted moving average (EWMA)</a> of the number of busy threads in the thread pool, sampled every second. The EWMA of the sampled thread pool utilization values is configured such that the sampled values of the past 10 seconds have the most effect on the thread pool utilization component of the ingestion load and samples older than 60 seconds have very negligible impact.</p><p>To estimate the resources required to handle the queued indexing requests in the thread pool, we need to have an estimate for how long each queued task can take to execute. To achieve this, each thread pool also provides an EWMA of the request execution time. The request execution time for an indexing request is the (wall-clock) time taken for the request to finish once it is out of the queue and a worker thread starts executing it. As some queueing is acceptable and should be manageable by the thread pool, we try to estimate the resources needed to handle the excess queueing. We consider up to 30s worth of tasks in the queue manageable by the existing number of workers and account for an extra thread proportional to this value. For example, if the average task execution time is 200ms, we estimate that each thread is able to handle 150 indexing requests within 30s, and therefore account for one extra thread for each 150 queued items.</p><p>Note that since the indexing nodes rely on pushing indexed data into the object store periodically, we do not need to scale the indexing tier based on the total size of the indexed data. However, the disk IO requirements of the indexing workload needs to be considered for the autoscaling decisions. The ingestion load represents both CPU requirements of the indexing nodes as well as disk IO since both CPU and IO work is done by the write thread pool workers and we rely on the wall clock time to estimate the required time to handle the queued requests.</p><p>Each indexing node calculates its ingestion load and publishes this value to the master node periodically. The master node serves the per node ingestion load values via the autoscaling metrics API to the autoscaler.</p><h3>Memory</h3><p>The memory metrics exposed by the autoscaling metrics API are node memory and tier memory. The node memory represents the minimum memory requirement for each indexing node in the cluster. The tier memory metric represents the minimum total memory that should be available in the indexing tier. Note that these values only indicate the minimum to ensure that each node is able to handle the basic indexing workload and hold the cluster and indices metadata, while ensuring that the tier includes enough nodes to accommodate all index shards.</p><p>Node memory must have a minimum of 500MB <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#_consider_additional_heap_overheads">to be able to handle indexing workloads</a>, as well as <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#shard-count-recommendation">a fixed amount of memory per each index</a>. This ensures all nodes can hold metadata for the cluster, which includes metadata for every index. Tier memory is determined by accounting for the memory overhead of the field mappings of the indices and the amount of memory needed for each open shard allocated on a node in the cluster. Currently, the per-shard memory requirement uses a fixed estimate of 6MB. We plan to refine this value.</p><p>The estimate for the memory requirements for the mappings of each index is calculated by one of the data nodes that hosts a shard of the index. The calculated estimates are sent to the master node. Whenever there is a mapping change this estimate is updated and published to the master node again. The master node serves the node and total memory metrics based on these information via the autoscaling metrics API to the autoscaler.</p><h2>Scaling the cluster</h2><p>The autoscaler is responsible for monitoring the Elasticsearch cluster via the exposed metrics, calculating the desirable cluster size to adapt to the indexing workload, and updating the deployment accordingly. This is done by calculating the total required CPU and memory resources based on the ingestion load and memory metrics. The sum of all the ingestion load per node values determines the total number of CPU cores needed for the indexing tier.</p><p>The calculated CPU requirement and the provided minimum node and tier memory resources are mapped to a predetermined set of cluster sizes. Each cluster size determines the number of nodes and the CPU, memory and disk size of each node. All nodes within a certain cluster size have the same hardware specification. There is a fixed ratio between CPU, memory and disk, thus always scaling all 3 resources linearly. The existing cluster sizes for the indexing tier are based on node sizes starting from 4GB/2vCPU/100GB disk to 64GB/32vCPU/1600GB disk. Once the Elasticsearch cluster scales up to the largest node size (64GB memory), any further scale-up adds new 64GB nodes, allowing a cluster to scale up to 32 nodes of 64GB. Note that this is not a hard upper bound on the number of Elasticsearch nodes in the cluster and can be increased if necessary.</p><p>Every 5 seconds the autoscaler polls metrics from the master node, calculates the desirable cluster size and if it is different from the current cluster size, it updates the Elasticsearch Kubernetes Deployment accordingly. Note that the actual reconciliation of the deployment towards the desired cluster size and adding and removing the Elasticsearch nodes to achieve this is done by Kubernetes. In order to avoid very short-lived changes to the cluster size, we account for a 10% headroom when calculating the desired cluster size during a scale down and a scale down takes effect only if all desired cluster size calculations within the past 15 minute have indicated a scale-down.</p><p>Currently, the time that it takes for an increase in the metrics to lead to the first Elasticsearch node being added to the cluster and ready to process indexing load is under 1 minute.</p><h2>Conclusion</h2><p>In this blog post, we explained how ingest autoscaling works in Elasticsearch, the different components involved, and the metrics used to quantify the resources needed to handle the indexing workload. We believe that such an autoscaling mechanism is crucial to reduce the operational overhead of an Elasticsearch cluster for the users by automatically increasing the available resources in the cluster when necessary. Furthermore, it leads to cost reduction by scaling down the cluster when the available resources in the cluster are not required anymore.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <dc:creator><![CDATA[Pooya Salehi,Henning Andersen,Francisco Fernández Castaño]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb2cd04a86700d5c2/6a17d715e3179138882d567d/016f5f2a90974dd53416d7d12f8776e802a0eef8-1440x823.png" length="0" type="image/png"/>
    <pubDate>Mon, 29 Jul 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>