<?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[Index Data - 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[Index Data - 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/blog/category/index-data</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/index-data</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/index-data.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Wed, 16 Sep 2026 16:06:49 GMT</lastBuildDate>
  <item>
    <title><![CDATA[One field, one copy: How Elasticsearch columnar storage drops the inverted index]]></title>
    <description><![CDATA[Storing each field once means no inverted index, so doc values now read in bulk and skippers let queries skip whole ranges of documents, while new mapping attributes control what each field is allowed to contain.]]></description>
    <content:encoded><![CDATA[<p>As part of the 9.5.0 release, Elasticsearch introduced <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar">columnar and logsdb_columnar index modes</a> in technical preview. Elasticsearch has had columnar storage using Lucene’s doc values since version 1.0.0. Lucene’s doc values power analytics and search functionalities, like group by and sorting by a field. So, what changes with the columnar index modes? </p><p>The changes are about storage and performance, along with the out-of-the-box (OOTB) experience. Up until 9.5.0, Elasticsearch operated as a document-based search engine by default. It could be set up to behave like a columnar system storage-wise, but that wasn’t the OOTB experience. <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar">Columnar index modes</a> make a number of fundamental changes that allow Elasticsearch to optimize columnar analytic and search use cases:</p><ul><li><p>Fields are stored once as doc values only and are no longer indexed by default.</p></li><li><p>New multi-value semantics. The original ordering of multiple values per field per document (for example, in arrays) is preserved by default.</p></li><li><p>Mappings are always flat, and object and passthrough fields in mappings are always auto-flattened.</p></li></ul><h2>How columnar index modes fit into Elasticsearch</h2><p>Many of the columnar index mode changes originate from <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data streams (TSDSs)</a>. As part of making TSDB a competitive metrics solution, we improved doc values format on disk and only store dimensions and metric fields once as doc values. We also improved query performance. TSDB is already columnar today. Essentially, this makes TSDB’s storage mode columnar. The lessons learned from TSDB are now being applied more broadly to Elasticsearch. </p><p>Note that columnar index modes are opt-in and columnar, and document-based indices can coexist in the same cluster. An enterprise search use case can use a document-oriented index mode, while a logging use case can use logsdb_columnar index mode and be fully columnar, all in the same cluster. In fact, there are currently seven index modes, and indices can all use them in the same cluster.</p><h2>How columnar storage stays fast without an inverted index</h2><p>Indexed fields, either an inverted index for string-based fields or block k-dimensional (BKD) tree for numeric fields, allow Elasticsearch to query or filter by field very efficiently. However, the cost for this is an additional expensive data structure that uses a lot of disk space and is expensive to build at index time and at merge time. With the columnar index modes, fields are no longer indexed by default, so what did we do for query performance to be still acceptable on fields that were no longer indexed?</p><p>One major change was improving doc values scanning performance. This is key and is the cornerstone that any columnar system relies on. Previously, the scanning of doc values was essentially document by document. Lucene’s doc values API only allowed for looking up one value at a time. Historically, this fit the execution model of a search engine. In our own doc value format, we build the capability to allow bulk reading of values for Elasticsearch Query Language (ES|QL) queries. Also over recent minor Lucene releases, Lucene doc values API added support for bulk reading. Without this, fast columnar scanning wouldn’t have been possible.</p><p>Secondly, we fully adopted <a href="https://www.elastic.co/search-labs/blog/docvaluesskippers-lucene-range-queries">doc value skippers</a>, a hierarchical skiplist over doc values. Contrary to an inverted index or a BKD tree, doc values skippers are lightweight data structures. At its core, a <em>skipper</em> allows queries to skip over a range of documents that don’t match a query. It can do this because it stores information, like min and max values. So, for example, when a range query is executed, an interval of documents can be skipped based on the intermediate result and a doc value skipper’s min and max values. The effectiveness of doc value skippers depends on the order in which documents are laid out on disk. This is why <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar#index-sorting">index sorting</a> should be enabled or altered to match the use case.</p><p>By significantly improving our columnar scanning and doubling down on doc values skippers, we’re able to avoid indexing fields by default. Note that a field can still be indexed; <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-index">the <code>index</code> mapping attribute</a> just defaults to <code>false</code> in columnar mode. The exceptions to this rule are text-based fields, which are still indexed by default. This is because text fields provide free text search, which includes text analysis, along with phrase and wildcard matching. This is different from just filtering.</p><h2>How Elasticsearch handles high and low cardinality fields</h2><p>When setting up a schema with string fields, an important configuration parameter is often <em>cardinality</em>; that is, whether many unique values or a few unique string values are expected. Many systems have dedicated field or column types that target low and high cardinality string fields.</p><p>Fields that have low cardinality are typically stored with a dictionary, containing all unique values. Then, for each row offset, the offset into the dictionary containing the term the row has is stored. This is often called an <em>ordinal</em>. For low cardinality fields, this works well, as storing an ordinal per document takes up much less space. Encoding techniques, like delta encoding, offset encoding, and bitpacking, work well for ordinals to compact the per-document storage to just a few bits. </p><p>However, for high cardinality fields, the dictionary and ordinal approach can work counterintuitively. If a larger percentage of the documents have a unique value, building the dictionary becomes expensive and storage savings diminish. The dictionary then becomes another level of indirection for reading values. This is why most systems in that case store values in a columnar fashion using block-based compression. For example, values of multiple rows are stored in 128KB blocks using a sliding-window dictionary-based compression algorithm (like zstandard or lz4). This, in general, is a simple and effective method to store higher cardinality fields and avoids building and maintaining a dictionary. </p><p>With document-based Elasticsearch, there are two ways to map a string: using either the keyword field mapping or one of the text-based field mappings. The former stores an inverted index and dictionary-based doc values. The latter only stores an inverted index. This is why, typically, a text field mapper is often used in combination with a keyword mapper as a <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/multi-fields">multi-field</a>. Also, keyword field mapper (as the name suggests) is meant for keywords or fields that have a lower cardinality and uses dictionary-based doc values implementation. However, in practice, keyword field mappers are also used for high cardinality.</p><p>In columnar mode, every field is only stored once by default. For keyword fields, this means only doc values are stored with no inverted index. Text-based fields now also store doc values and an inverted index by default. Text-based field mappers are different from the keyword field mapper, as these are not automatically used via Elasticsearch’s dynamic mapping logic for columnar indices, and therefore text fields keep storing an inverted index by default.</p><p>For both keyword- and text-based fields, we didn’t choose to expose a cardinality mapping attribute. It’s not always possible to know ahead of time whether a field is low or high cardinality. When flushing and merging segments to disk, Elasticsearch sees all values and can determine the cardinality of a field. This is why we’re choosing to automatically determine whether the usage of a dictionary and ordinal-based encoding is beneficial over block-based compression using a simple cardinality threshold. If a field is below this threshold, dictionary and ordinal-based encoding is used; otherwise block-based compression is used. This simplifies configuration of the mappings and makes it possible to automatically optimize storage as data evolves, since some segments may use dictionaries while others may use blocks for the same field. However, this is currently not ready yet and so, as part of 9.5.0, in columnar mode, both keyword- and text-based field mappers store values in doc values in a block-based compressed layout on disk.</p><p>The two approaches compare as follows:</p><p>
</p><p><strong>Dictionary and ordinal encoding</strong></p><p><strong>Block-based compression</strong></p><p>Suits</p><p>Low cardinality fields</p><p>High cardinality fields</p><p>What’s stored</p><p>A dictionary of unique values, plus one ordinal per document</p><p>Values for many documents compressed together in blocks</p><p>Compression</p><p>Delta encoding, offset encoding, and bitpacking reduce each ordinal to a few bits</p><p>Sliding-window dictionary compression, such as zstandard or lz4, typically over 128KB blocks</p><p>Read path</p><p>Resolve the ordinal, and then look up the value in the dictionary</p><p>Decompress the block, and then read the value directly</p><p>Cost as cardinality rises</p><p>Dictionary grows large, savings shrink, and the extra indirection stays</p><p>Stable, with no dictionary to build or maintain</p><p>Used in columnar mode tech preview</p><p>Not yet</p><p>Yes, for both keyword and text fields</p><h2>Columnar mapping attributes: multi_value, nullability, on_failure</h2><p>The columnar index modes provide more control over how data is stored as doc values. By default, Elasticsearch is lenient and accepts all non-malformed values (for example, nulls and multiple values per field and document). If documents have fields with multiple values per document or no value, doc values store additional data structures to deal with them and therefore implicitly increase costs. </p><p>With columnar, new mapping attributes provide additional control. Note that these new mapping attributes are currently only available with the columnar index modes but will eventually also be available for all index modes.</p><p>Three new mapping attributes are involved:</p><p><strong>Attribute</strong></p><p><strong>Default</strong></p><p><strong>Enforces</strong></p><p><strong>On violation</strong></p><p><strong>Available</strong></p><p><code>multi_value</code></p><p><code>true</code></p><p>One value per document per field</p><p>Document indexing fails</p><p>9.5.0</p><p><code>nullability</code></p><p><code>true</code></p><p>Field must have a value</p><p>Document indexing fails</p><p>9.5.0</p><p><code>on_failure</code></p><p><code>fail</code></p><p>How the above failures are handled</p><p>Sets <code>fail</code> or <code>ignore</code> behavior</p><p>Next minor release</p><h3>multi_value: Enforcing single-valued fields</h3><p>By default, Elasticsearch accepts multiple values per document. To understand the implications of this, we first should take a look at how Elasticsearch (using Lucene’s doc values) stores a dense numeric field where all documents have a single value: </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3abb1f6fb702b165/6a9fa698893681d9ef3df7d6/unnamed.png" alt="Columnar storage doc values layout: value blocks and a block index resolve which value belongs to a docId" /><p>With this layout, all values are stored in blocks. The number of values per block depends on the index mode but is typically 128 values and is always the same within an index. All values in a block are encoded using various encoding techniques, like delta encoding and bit packing, so each block can have a different size, depending on how well the encoding techniques compress the values. This is why a block index is required. </p><p>Lucene has the notion of a docid (internal numbering for a document), which is essentially a row identifier.</p><ol><li><p>Queries produce matching docids.</p></li><li><p>In case of a dense field, the block id can be resolved from the docid directly.</p></li><li><p>The offset of a block can be resolved from the block index.</p></li><li><p>Once that has been looked up, the target block gets decoded and all values are available.</p></li><li><p>Finally, from docid, the ordinal within the decoded values array can be resolved, which produces the final value.</p></li></ol><p>Now let’s have a look at how the data layout changes when documents have multiple values per document:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9d5ca280a19c7a70/6a9fa6edee57e5dec9051db9/unnamed.png" alt="Multi-value doc values layout in columnar storage: offsets map a docId to values spanning value blocks" /><p>To determine how many values belong to a single docid, an offset lookup is required.</p><p>In this case, a docid has one or more offsets. Each offset points to a block index. Values for a single document are adjacent but can stretch over blocks. In general, compaction of values works well in blocks because values are similar. However, multi-value fields can cause the compaction of values to be less efficient if the number of values per field and document is large and values aren’t similar. This and the additional storage of offsets result in multi-value fields typically having a higher storage footprint on disk.</p><p></p><p>If a field is truly single-valued, you may want to enforce this property. <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values#doc-values-multi-value">The <code>multi_value</code> mapping attribute</a> makes that possible now. An example is a log level field. Logs typically have one log level (such as debug, info, or error). Enforcing that this field is single-valued in your mappings can help avoid accidentally using more storage than anticipated. A mapping snippet example that disallows the field <code>log.level</code> to have multiple values per document:</p>{
	"properties": {
		"log.level": {
			"type": "keyword",
			"multi_value": false
		}
	}
}<p></p><p>Note that even if a field allows multiple values, this doesn’t mean an offset lookup is stored. This only happens when a Lucene segment has at least one document with two or more values. The <code>multi_value</code> mapping attribute exists just for enforcement.</p><h3>nullability: Requiring every document to have a value</h3><p>By default, Elasticsearch accepts documents with fields that have no value or null value. Just as  multiple values per document require additional accounting, documents with no value require additional accounting to identify which of them have at least one value.</p><p>Doc values store a docid to offset lookup (known as IndexedDISI in Lucene) in case not all documents have a value in a segment. The offset either points directly to the block index for single-valued fields or the offset lookup in case of multi-valued fields. This lookup is compact compared to the value blocks being stored. However, if it were to be created for fields that should have at least one value per document, that would be a waste.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values#doc-values-nullability">The <code>nullability</code> mapping attribute</a> allows you to control whether documents are allowed to have no value. Just like the <code>multi_value</code> mapping attribute, the <code>nullability</code> attribute exists for enforcement.  Following is a mapping snippet example that requires the <code>log.level</code> field to have a value:</p>{
  "properties": {
     "log.level": {
        "type": "keyword",
        "nullability": false
     }
  }
}<h3>on_failure: What happens when validation fails</h3><p>What happens if a document has multiple values for a field and if the <code>multi_value</code> mapping attribute is set to <code>false</code> or when a field is mapped with nullability set to <code>false</code> and a document doesn’t have that field? At the moment, indexing such documents will fail with a bad request error.</p><p>As part of the next minor release, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values#doc-values-on-failure">the <code>on_failure</code> mapping attribute</a> will be available. This allows you to indicate how to handle these validation failures, on a per-mapped field basis. This will support two values:</p><ol><li><p>Fail: Fail indexing of the entire document with a client error. This is the current behavior in Elasticsearch 9.5.0.</p></li><li><p>Ignore: Ignore the validation error, mark the field as ignored, and store values for that field in a hidden field so that it can be introspected when requesting the source. </p></li></ol><h2>Trying the columnar index modes</h2><p>The <a href="https://www.elastic.co/docs/reference/elasticsearch/columnar">columnar index modes</a> are still under active development, but we encourage you to give them a test drive. As we prepare the columnar index modes for general availability (GA), we’ll add more performance and efficiency improvements. We believe that by adapting a columnar mindset, many use cases will benefit from being more cost effective or having better performance characteristics.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/columnar-storage-elasticsearch-index-modes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/columnar-storage-elasticsearch-index-modes</guid>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Mappings]]></category>
    <dc:creator><![CDATA[Martijn van Groningen]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef1303bd86788300/6a9fa5cdf08ee10715855390/unnamed.png" length="0" type="image/png"/>
    <pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[ Backfill time series data in Elasticsearch: Load months of historical metrics through the bulk API]]></title>
    <description><![CDATA[Elasticsearch works out the time boundaries and creates the past backing indices as the documents land, so a historical data migration runs on your normal ingest path.]]></description>
    <content:encoded><![CDATA[<p>You can now write documents with past timestamps straight into Elasticsearch <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data streams (TSDB)</a>. Send months of historical metrics through the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">bulk API</a>, the <a href="https://www.elastic.co/docs/manage-data/ingest/otlp-endpoint">OpenTelemetry Protocol (OTLP) endpoint</a>, or the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/tsds-ingest-prometheus-remote-write">Prometheus remote write endpoint</a>. Elasticsearch creates the past backing indices as the documents arrive, computing each index's time boundaries and attaching it to the data stream. Backfilled documents are stored exactly like live ones, with columnar storage and write-time deduplication, along with up to <a href="https://www.elastic.co/blog/70-percent-storage-savings-for-metrics-with-elastic-observability">70% storage savings</a>. Time series data backfill ships in Elasticsearch 9.5, disabled by default, and turns on with one cluster setting. How far back you can write depends on your lifecycle configuration, since backfill doesn’t apply to indices that are already read-only as a result of downsampling or a searchable snapshot.</p><h2>How historical metrics were loaded before backfill</h2><p>Even if loading historical metrics isn’t a very common use case, it’s an important step when teams are adopting TSDB. Two scenarios have been the most prominent: bootstrapping a new time series data stream and migrating data from a different system or data stream to a time series one.</p><h3>Bootstrapping a new time series data stream</h3><p>You want to start a new time series data stream with a week of historical data so you have something meaningful to query from the start. With existing tooling, you had to set <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/time-series"><code>index.look_back_time</code></a> to the seven-day maximum in the index template, and all historical data would land in a single backing index. For anything beyond seven days, you needed to create past backing indices manually.</p><h3>Migrating metrics from another system</h3><p>You have months worth of metrics stored on a different system and want to move your full dataset to TSDB. You need to load months of metrics history alongside live ingestion. The workaround was to manually create all the necessary past backing indices with the right <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/time-series"><code>time_series.start_time</code></a> and <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/time-series"><code>time_series.end_time</code></a> and to index into it directly using the index name. You then attached it to the data stream via the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/modify-data-stream">modify data stream API</a>. It worked, but it required understanding the index time semantics and repeating the steps for each time window, along with coordinating that process around ongoing writes.</p><p>We wanted both scenarios to feel as close to normal bulk indexing as possible.</p><h2>What time series data backfill changes</h2><p>In 9.5, Elasticsearch can create backing indices covering past time ranges, which extends the eligible write window backward.</p><p>The eligible write windowis the range of <code>@timestamp</code> values that a time series data stream accepts for new documents. </p><p>In the past, the eligible write window was determined only by the existing writable backing indices at the moment the request was received by Elasticsearch.</p><p>In 9.5, Elasticsearch can expand the eligible write window in the past by creating backing indices. This converts the eligible write window to a sliding window extending from the present back to the first read-only or destructive lifecycle action. Common examples of these actions, which are typically defined within your lifecycle configuration, are <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/downsampling-concepts">downsampling</a> or <a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">searchable snapshots</a>. Examples also include <a href="https://www.elastic.co/docs/manage-data/lifecycle/data-stream/tutorial-data-stream-retention">retention configurations</a>. </p><p>So, given that loading historical data is enabled in a cluster, the eligible write window of the data stream with the following lifecycle configuration is determined by the downsampling action, because it’s the first action that makes backing indices read-only. So, for this data stream Elasticsearch accepts documents whose <code>@timestamp</code> is no older than three months.</p>GET _data_stream/metrics/_lifecycle
{
  "enabled": true,
  "downsampling": [{ "after": "90d", "fixed_interval": "10m" }],
  "data_retention": "365d"
}<h3>Why loading historical data into TSDB is hard</h3><p>TSDB consists of data streams optimized for timestamped measurements. It uses a columnar storage layout and enforces immutable dimensions. It also organizes data into time-bound backing indices; each <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-bound-tsds">index covers a specific time range</a> and accepts only documents whose <code>@timestamp</code> falls within it.</p><p>As time passes, rollover creates new backing indices to cover upcoming ranges. Until this release, there was no corresponding mechanism for the past. Creating indices in the past is tricky because historical data might span over a long period of time and can arrive at Elasticsearch out of order. Consequently, Elasticsearch cannot determine the write timeframe that its backing index should cover. Our solution to this is to use a preconfigured interval and lazily create past backing indices.</p><h2>How Elasticsearch creates past backing indices</h2><p>When a document is detected whose timestamp isn't covered by any existing backing index, Elasticsearch determines the time boundaries for the missing indices and creates them. It then adds them to the data stream in a single atomic operation. </p><p>Lazily creating the indices ensures that a single request in the past won’t overwhelm the cluster by requiring the creation of 300 indices all at once. It also doesn’t create indices before there are docs to write into them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a3c89f8c228a3b6/6a9a5b6532b530b1df6d23a0/unnamed.png" alt="Time series data backfill timeline: past backing indices accept documents within the retention limit, rejecting older ones" /><h3>Proactive vs. reactive: How we chose the index creation approach</h3><p>We explored two ways to detect when a past backing index needs to be created.</p><p>The first is proactive. Inspect each incoming document's timestamp before routing, and create any missing past backing indices up front. This keeps the write path clean. By the time a document is routed, the index it needs already exists. It does require the data stream to already exist with at least one time series backing index, since that's what we inspect to determine the eligible write window and the time boundaries of the new index. The downside is that it adds work to every bulk request targeting a time series data stream, even requests that contain no past timestamps and need no backfill at all.</p><p>The second is reactive. Let the document fail the normal indexing, intercept that failure, create the missing index, and retry. This avoids any overhead on the common case, since the extra work only happens when a mismatch actually occurs. The tradeoff is more complexity in the failure handling path and a retry on every backfill document.</p><p>We ran performance tests on the proactive approach against bulk requests with no past timestamps and found no measurable regression. The overhead of inspecting timestamps turned out to be negligible. That settled it. Proactive creation is simpler and consistent with how index auto-creation already works in Elasticsearch. Plus, it adds no measurable cost to the workloads that don't use backfill.</p><h3>How Elasticsearch determines past index boundaries</h3><p>Each new past backing index has three properties to compute: its duration, its start time, and its end time.</p><p><strong>Property</strong></p><p><strong>How it's set</strong></p><p><strong>Constraint</strong></p><p>Duration</p><p>Defaults to one day, configurable via the cluster setting <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/miscellaneous-cluster-settings#time-series-data-stream"><code>data_streams.past_tsdb_index_interval</code></a></p><p>Minimum one hour. If the triggering timestamp falls in a gap up to 1.3 times the configured duration, Elasticsearch collapses it into a single bridging index rather than creating many tiny ones.</p><p>Start time</p><p>Anchored to the start of the next existing backing index, working backward in multiples of the configured duration</p><p>Increased to match the end time of the previous neighboring index, where they would otherwise overlap.</p><p>End time</p><p>Start time plus the configured duration</p><p>Reduced to match the start time of the next index, where they would otherwise overlap.</p><h3>Handling concurrent writes</h3><p>In a distributed setup, multiple nodes can receive bulk requests with overlapping past timestamps at the same time. Each node collects the timestamps that aren’t matching any of the existing indices and sends a request to the master node. </p><p>The master node executes a cluster update that sorts them and then, one by one, checks whether the timestamp is covered by an existing or newly created index. Otherwise, it issues a new create index request with the time boundaries calculated as described above. The cluster updates are always sequential and guaranteed to produce valid cluster states, so new indices are guaranteed to not overlap with existing indices.</p><h3>How lifecycle age works for backfilled indices</h3><p>Past backing indices hold old data but are new indices. Without an adjustment, lifecycle features would apply downsampling and retention based on when the index was created rather than when the data is from. We account for this by using the <code>index.time_series.end_time</code> as the <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/data-stream-lifecycle-settings#_index_level_settings"><code>index.lifecycle.origination_date</code></a>. As a result, the age of the index as perceived by both <a href="https://www.elastic.co/docs/manage-data/lifecycle/data-stream">data stream lifecycle</a> and <a href="https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management">index lifecycle management (ILM)</a> is based on the age of its data and not its creation time.</p><h2>How to use time series data backfill</h2><h3>How to enable time series data backfill</h3><p>Backfill support ships disabled by default. <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cluster-put-settings">Enable it at the cluster level</a>:</p>PUT _cluster/settings
{
"persistent": {
"data_stream.past_tsdb_index_creation_enabled": true
	}
}<h3>Bootstrapping with historical metrics</h3><p>To load historical data into a new time series data stream:</p><ol><li><p><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-put-index-template">Create your index template.</a> </p></li><li><p><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-create-data-stream">Initialize your data stream.</a> (This is an important step because an existing data stream is a requirement for creating past backing indices.)</p></li><li><p>Start indexing. </p></li></ol><p>Past backing indices are created automatically as documents with historical timestamps arrive, each covering one day of data by default. No additional configuration is needed.</p><h3>Data migration into an existing data stream</h3><h4>Migrating data within the eligible write window</h4><p>For data that falls within the eligible write window of your data stream, point your migration pipeline at the data stream and let Elasticsearch manage the rest.</p><h4>Migrating data beyond a read-only action</h4><p>For data older than the write window (for example, you're migrating 18 months of metrics but downsampling kicks in after seven days), you need a separate data stream without read-only lifecycle actions. Retention isn’t an issue since the data would be deleted anyway. The pattern is:</p><p>1. Create an index template for the historical data stream, using the same mappings as the original but without a lifecycle:</p>PUT _index_template/my-metrics-historical
{
  "index_patterns": ["metrics-historical-*"],
  "data_stream": {},
  "template": {
    "settings": { "index.mode": "time_series" },
    "mappings": {
      "properties": {
        "sensor_id": { "type": "keyword", "time_series_dimension": true },
        "temperature": { "type": "half_float", "time_series_metric": "gauge" },
        "@timestamp": { "type": "date" }
      }
    }
  }
}<p>2. Create the historical data stream. If this step isn’t executed, the first indexing request might fail. During the first indexing request, Elasticsearch can create the data stream but it cannot yet create any past backing indices, so indexing a historical document might fail. Creating the data stream explicitly ensures that all indexing requests will be accepted:</p>PUT _data_stream/metrics-historical-2024<p>3. Index historical data into the historical data stream while current data continues flowing into the original.</p><p>4. When the load is complete, add lifecycle. This is only supported by data stream lifecycle since this feature functions on a data stream level:</p>PUT _data_stream/metrics-historical-2024/_lifecycle
{
"enabled": true,
"downsampling": [{ "after": "7d", "fixed_interval": "10m" }]
}<p>5. Query across both data streams with a wildcard pattern (<code>my-metrics*</code>) or a data stream alias.</p><p>6. If retention is configured, delete the historical data streams when their data expires. Data stream lifecycle will delete the data but it won't clean up the data stream itself.</p><p>As you see, the historical data needs to fit on the target tier as a whole because lifecycle will be enabled after the data is loaded. If you have a large historical import, you might choose to split it into batches. Make sure each batch can fit on the target tier as a whole at the time of indexing, to avoid running your cluster out of disk space. Lifecycle will start processing the batch's indices as soon as it's enabled, but it will need time to process the whole backlog.</p><h2>Protecting the cluster during large migrations: Downsampling floodgate</h2><p>When data stream lifecycle runs against a data stream with many indices that all qualify for downsampling, it queues them simultaneously. Downsampling is CPU and I/O intensive; it reads and rewrites all data in an index. Queuing dozens of operations at once can overwhelm the master node with persistent task updates while it coordinates them.</p><p>The downsampling floodgate scenario could occur before backfill support (for example, when adding a lifecycle policy to an existing data stream with months of accumulated data). Backfill makes it more likely by design.</p><p>In 9.5 and serverless, we added flood protection to data stream lifecycle. It now tracks how many indices per data stream are actively being downsampled. If that count reaches a threshold, data stream lifecycle pauses queuing further operations for that data stream until the count drops. The threshold is configurable via the cluster setting <a href="https://www.elastic.co/docs/reference/elasticsearch/configuration-reference/data-stream-lifecycle-settings#_cluster_level_settings"><code>data_streams.lifecycle.downsampling.max_indices_in_progress</code></a>. Other data streams aren't affected.</p><h2>Limitations and prerequisites of time series data backfill</h2><ul><li><p>Backfill doesn’t apply to read-only indices. If downsampling or a searchable snapshot transition has already run on a time period, documents for that period are still rejected.</p></li><li><p>The feature requires a preexisting time series data stream with at least one time series backing index.</p></li><li><p>System data streams are excluded.</p></li><li><p>Replicated data streams rely on the leader data stream, so no direct backfilling is possible.</p></li><li><p>Scaling remains your responsibility. Loading months of data can trigger significant storage usage, force merge operations, and lifecycle activity in parallel. Check that your cluster has the headroom to manage it before starting.</p></li></ul><h2>Conclusion</h2><p>Prior to the Elasticsearch 9.5 release, loading historical data into TSDB was a manual process. By automating the generation and management of past backing indices, we aim to transform historical data migration to a native capability of your standard ingest pipelines. The inherent complexity of managing time-bound indices remains, but it has transitioned from a user responsibility into an internal Elasticsearch function. Whether you’re bootstrapping a fresh data stream or migrating extensive historical datasets, the platform now handles the heavy lifting, allowing you to focus on analyzing your metrics. We look forward to seeing how these improvements streamline your adoption of TSDB.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/time-series-data-backfill</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/time-series-data-backfill</guid>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Mary Gouseti]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc78b8fb3f37f3ee8/6a9a5ad6ecbe18174b1e37ac/unnamed.png" length="0" type="image/png"/>
    <pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[ES95: Adaptive Compression for Elasticsearch Time-Series Metrics]]></title>
    <description><![CDATA[ES95 is Elasticsearch 9.5's new adaptive time series codec that cuts @timestamp storage by 92% and floating point fields by up to 74%, with zero configuration.]]></description>
    <content:encoded><![CDATA[<p><em>The best compression strategy is the one that understands your data.</em></p><p>Observability workloads are storage-intensive by nature, and the composition of that storage determines both cost and query performance. <code>ES95</code> introduces adaptive compression: rather than applying the same encoding to every numeric field, it automatically selects the encoding that best matches each field's structure. The result is a 33.6% reduction in total doc-values storage, 19% to 74% reduction on floating-point gauge metrics and a 92% reduction in <code>@timestamp</code>. No configuration or migration required.</p><h3>Observability data is storage-intensive</h3><p>Data processing systems are rarely limited by how fast they can compute. They’re limited by how fast they can move bytes: off disk, across the network, and through the memory hierarchy. Compression is how a storage engine trades CPU time for memory bandwidth, spending comparatively cheap CPU cycles so fewer bytes have to travel through the parts of the system that are usually constrained. In a read-heavy system like Elasticsearch, that trade-off pays back every time data is queried, often long after it was written.</p><p>Storage size and query performance move together; fewer bytes on disk means fewer bytes to read on every range query, every aggregation and every dashboard load. Compression is not just about saving storage. Every byte that is never written is also a byte that never has to be read.</p><p>The right encoding depends on the structure of the values themselves, and the largest wins come from exploiting the structure already present in the data rather than squeezing an opaque stream of bytes. Few workloads expose that structure more clearly than observability metrics.</p><p>A single host reports hundreds of metrics every few seconds, including CPU utilization, memory ratios, request latencies, and network throughput. Multiply that by thousands of hosts across weeks of retention, and the bytes accumulate fast. Most of that volume is structured but not uniform: timestamps arrive at near-constant intervals from thousands of concurrent series, counters increase monotonically, while gauges like <code>23.47</code> or<code>1.15</code> are short decimal measurements.</p><p>A fixed compression approach cannot adapt to that variety. A timestamp column and a floating-point gauge column compress through fundamentally different techniques, but a codec that applies the same approach to both will necessarily handle one of them poorly. For most of Elasticsearch's time-series codec history, gauges were on the losing end of that trade-off.</p><h3>The structure the old codec was not built to exploit</h3><p>Elasticsearch stores time-series numeric values in <em>doc values</em>: a column-oriented structure where all values for the same field sit adjacent on disk. That adjacency makes compression possible: the codec compares consecutive values of the same field, finds patterns, and exploits them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2386e2eb944a502a/6a85671fa8b3235ccccbf4f9/unnamed.png" alt="Row-oriented vs column-oriented storage in Elasticsearch time series indices showing how field values cluster on disk" /><p>The <a href="https://www.elastic.co/search-labs/blog/time-series-data-elasticsearch-storage-wins">time-series codec before ES95</a> applied the same fixed encoding to every numeric field: delta encoding followed by normalization, GCD (greatest common divisor) reduction, and bit-packing. Each encoding technique activated where it helped and skipped where it would not. For timestamps and integer counters, this approach was remarkably effective. For floating-point gauges, it could find almost nothing to work with.</p><p>The reason is how they’re stored. To support range queries, Elasticsearch stores floating-point values as integers that preserve numeric ordering. A change of 0.01 in a CPU percentage reading translates to a jump of trillions in that integer space. The codec sees those large jumps and has no strategy to further reduce their footprint. Storage stays near the original eight bytes per value.</p><p>The codec was doing the right thing with the representation it had, but the latter was chosen for querying, not compression, and the goals conflict at the bit level.</p><h3>The cost of a fixed format</h3><p>A compression stage for floating-point values was already on the roadmap, so the interesting part wasn’t the algorithm. The obstacle was architectural.</p><p>The previous codec baked its compression approach into the storage format. Adding a new encoding meant changing the meaning of existing bytes on disk, which forced a format migration, a rollout that can last weeks or months in large production clusters. Over time, that migration burden constrains codec development itself. The question stops being <em>Is this a good compression idea?</em> and becomes <em>Is it worth another format migration?</em> That rigidity limits the cadence of codec evolution and leads to missed compression improvements.</p><h3>The right encoding without configuration</h3><p><code>ES95</code> solves this at the architecture level for time-series indices. Each field's encoding is no longer baked into the format. It is selected automatically at write time, based on what the field mapping already declares: the field's name, its data type, and its metric role. Timestamps are encoded differently than counters. Counters are encoded differently than gauges. <code>ES95</code> encodes all of them, and it chooses the right strategy for each.</p><p>Users already tell Elasticsearch everything the codec needs to know. The mapping describes the data; the codec chooses the compression strategy.</p><p>Compression strategy is a codec concern, not a user concern.</p><p>The alternative would have been to expose per-field encoding selection as a configuration parameter, letting users opt in to better compression for specific fields. That would shift the burden of knowing which encoding fits which data type onto those least equipped to make that call and would guarantee that most deployments never see the benefit. <code>ES95</code> keeps that decision inside the codec, where it belongs. This matters most in managed and serverless deployments, where users expect the system to automatically make optimal storage decisions.</p><h3>The timestamp result nobody planned for</h3><p>With the adaptive architecture in place, the team set out to ship the planned float-compression algorithm. Before it arrived, the architecture proved itself by substantially improving compression for timestamps.</p><p>A time-series index is sorted first by its time-series identifier (<code>_tsid</code> constructed by the metric’s dimensions) and then by timestamp within each series. Timestamps on disk aren’t one smooth sequence; there are many smooth sequences laid end to end, one per series, with a large jump at every boundary where one series ends and the next begins.</p><p>The codec compresses data in fixed-size blocks without regard to those series boundaries. A block straddling a series boundary holds timestamps from two different series. The jump between them breaks monotonicity, reducing delta encoding effectiveness on blocks spanning different time series. A block that would otherwise compress to near-zero bits per value ended up needing nine or more, because bit-packing encodes every value in a block using the same fixed number of bits, so one large jump sets the cost for all of them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd7576b4b4a9e1c2d/6a85677d1eb9e5448c2c3336/unnamed.png" alt="SplitDelta encoding splits compression blocks at series boundaries, cutting @timestamp storage by 92.4%" /><p>In the ideal case, every block belongs to a single series: timestamps increase at near-constant intervals, delta encoding captures the regularity, and bit-packing compresses the result to near-zero bits per value. With few series, boundary blocks are rare and the overhead barely registers. On an observability cluster ingesting millions of documents across thousands of series, that changes. The cost scales along two dimensions: series count and data density. More series means more boundary events; sparser series means multiple jumps packed into single blocks. In high-churn environments, both compound, and boundary blocks accumulate into a standing tax on the most-read field in any time-series workload. It’s why <code>@timestamp</code> storage grew faster than the data that produced it.</p><p>The fix was to detect series boundaries and treat each run as its own independent sequence. Instead of trying to encode across the jump between two series, which forces every value in the block to pay the storage cost of that one large jump, each run is compressed on its own terms. The boundary simply becomes a seam: the jump is never seen by the encoder on either side.</p><p>That encoding is called <code>SplitDelta</code>. <code>@timestamp</code> and monotonic long counters now use it by default. No format change. No migration. Existing segments retain legacy encoding.</p><p>On the high-cardinality <a href="https://github.com/elastic/rally">Elasticsearch Rally</a> benchmark, that single unplanned encoding cut counters storage by 20%–30% and <code>@timestamp</code> storage by 92.4%, from 1.03 GB to 79 MB. Gigabytes to megabytes, and no, that isn’t a typo.</p><p>The pluggable pipeline had already paid for itself. <code>SplitDelta</code>, which wasn’t part of the original plan, slotted in without a format change or migration before ALP even shipped.</p><h3>ALP: recovering the decimal that was always there</h3><p>Most floating-point metrics can be expressed as short decimals with no loss of accuracy: CPU utilization at <code>23.47</code>, load average at <code>1.15</code>. <a href="https://dl.acm.org/doi/10.1145/3626717">ALP</a> (Adaptive Lossless floating-Point compression) recovers that decimal structure from the floating-point representation, converting values into integers that the existing pipeline already handles well. <code>ES95</code> feeds ALP's output into the same mature integer compression pipeline used for timestamps and counters, extracting additional savings rather than treating ALP as a standalone encoding. Values that don’t fit ALP's model (such as irregular high-precision floats or special values) fall back to direct bit-packing or the original representation without degrading the rest of the block.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt056bdb51022428c9/6a85689f4acc96ce082471b1/unnamed.png" alt="ALP converts floating-point time series metrics to integers for compression through the Elasticsearch encoding pipeline" /><p><code>ALP</code> lets Elasticsearch treat floating-point metrics according to the structure they actually contain rather than the binary representation they happen to use. It’s applied automatically to double-valued gauge fields, selected by field type and metric role, through exactly the door the architecture had built for it.</p><h3>What the numbers say</h3><p>Here’s what the <a href="https://github.com/elastic/rally">Elasticsearch Rally</a> benchmark looks like on a high-cardinality workload containing 2.26 billion data points. Results are from an internal <code>tsdb-metricsgen</code> benchmark.</p><p><strong>Field or metric</strong></p><p><strong>Storage reduction (%)</strong></p><p><code>@timestamp</code></p><p><strong>−92.4%</strong></p><p><code>cpu.load_average.5m</code></p><p><strong>−74.3%</strong></p><p><code>system.cpu.utilization</code></p><p><strong>−63%</strong></p><p><code>memory.utilization</code></p><p><strong>-19%</strong></p><p>Total doc values</p><p><strong>−33.6%</strong></p><p>That overall 33.6% reduction deserves context.</p><p>A time-series index contains more than metrics. Every data point also carries the labels that identify the series: host names, IP addresses, regions, container IDs. Those dimension fields are stored as keywords. <code>ES95</code> doesn’t target dimensions.</p><p>On this benchmark, two dimension fields, <code>host.ip</code> and <code>host.mac</code>, accounted for 44% of doc-values storage after <code>ES95</code> ran. The 33.6% total reflects that mix. The per-field breakdown is the honest picture. Compression for dimension fields is an active area of work.</p><p>The per-field variation is the most convincing result. Some gauges shrank by nearly three quarters, while others moved by less than a fifth. That spread is direct evidence that <code>ES95</code> matches compression to the structure actually present in each field. A fixed encoding treats every field identically and misses most of those wins.</p><h3>Better compression without extra configuration</h3><p>The storage reductions from <code>SplitDelta</code> and <code>ALP</code> are the most visible results of <code>ES95</code>. The more consequential result is the architecture that produced them.</p><p>Before <code>ES95</code>, every new compression technique required a format evolution. That reality shaped which ideas were practical to pursue. Today, new encodings become implementation decisions inside the codec rather than migration projects. Existing data never needs to move, and users gain better compression on newly written data simply by upgrading Elasticsearch. <code>SplitDelta</code> and <code>ALP</code> are the first encodings to benefit from this architecture. They will not be the last.</p><p>Asking users to choose compression algorithms would only duplicate information Elasticsearch already has. There are no per-field compression parameters to tune, and no expert knowledge is required to get good storage efficiency. Different fields get different strategies because <code>ES95</code> understands what kind of data each field contains, not because a user configured it. As the codec evolves, those decisions evolve with it. The API does not.</p><p>In Elasticsearch Serverless, good defaults are part of the product. Users expect the system, not configuration, to make storage decisions. <code>ES95</code> is designed to honor that expectation: encoding that starts right and gets better over time.</p><h3>The compression was always there</h3><p><code>ES95</code> establishes a new standard for how time-series codec evolution works. New encodings become implementation decisions, not migration projects. Users get better compression on newly written data with every Elasticsearch upgrade.</p><p>The compression was already in the data. <code>ES95</code> just removed what was hiding it.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/time-series-database-compression-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/time-series-database-compression-elasticsearch</guid>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Salvatore Campagna]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt27b1a5308344647f/6a8566caf9838a4c963bea55/unnamed.png" length="0" type="image/png"/>
    <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Two lines of JSON to replace your ILM policy: data stream lifecycle adds frozen tier support]]></title>
    <description><![CDATA[In Elasticsearch 9.5, frozen_after in data stream lifecycle moves indices to searchable snapshots on object storage on their own, keeping them queryable alongside downsampling and retention.]]></description>
    <content:encoded><![CDATA[<p>Data stream lifecycle in Elasticsearch 9.5 can move backing indices to the frozen tier as searchable snapshots on object storage, with no ILM policy required. Add frozen_after next to <code>data_retention</code> and optional downsampling in a few lines of JSON, or set it in Kibana. The feature is generally available in 9.5.</p><h2>How to configure frozen_after in data stream lifecycle</h2><p><code>frozen_after</code> sits at the top level of the lifecycle, next to <code>data_retention</code> and <code>downsampling</code>.</p>PUT _data_stream/my-data-stream/_lifecycle
{
"data_retention": "90d",
"frozen_after": "30d"
}<p>That's the whole feature, at the API level. Indices in <code>my-data-stream</code> stay on hot for 30 days, then move to frozen for the remaining 60. After 90 days they're deleted, and the backing snapshot goes with them.</p><p>It composes with the rest of the lifecycle, including downsampling:</p>PUT _data_stream/my-data-stream/_lifecycle
{
  "data_retention": "90d",
  "frozen_after": "30d",
  "downsampling": [
    { "after": "1d", "fixed_interval": "1h" }
  ]
}<p>Same options in an index template:</p>PUT _index_template/my-index-template
{
"index_patterns": ["my-data-stream*"],
"data_stream": {},
"template": {
"lifecycle": {
"data_retention": "90d",
"frozen_after": "30d"
}
  }
}<p>The order of values is enforced: <code>frozen_after</code> has to be less than <code>data_retention</code> and greater than any <code>downsampling.after</code>. The API rejects configurations that don't make physical sense.</p><h3>Where frozen tier data is stored: the default snapshot repository</h3><p>Frozen tier data is held as partially-mounted searchable snapshots, which means DLM needs a snapshot repository to write into. Rather than make you choose a repository per lifecycle, 9.5 introduces a <a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/self-managed#snapshot-repo-default">cluster-level default snapshot repository</a>.</p>PUT _cluster/settings
{
  "persistent": {
    "repositories.default_repository": "my-snapshot-repo"
  }
}<p>DLM uses this repository for every frozen tier index in the cluster. On Elastic Cloud Hosted (ECH), the default is pre-populated with <code>found-snapshots</code> so existing clusters work out of the box. You can change it to a repository you control if you'd rather keep your frozen data in a bucket you own (useful if you want object versioning, lifecycle backups to Glacier, or anything else that needs bucket-level access). Wherever you can set <code>frozen_after</code> in Kibana, the UI shows the current default repository inline and links to the place to change it, so you can see where frozen data will be written.</p><p>If the cluster doesn't have a default repository configured, you can still write a lifecycle with <code>frozen_after</code>. The API accepts it but returns a warning:</p>{
  "acknowledged": true,
  "warnings": [
    {
      "message": "No default snapshot repository has been configured. Data will not be moved to the frozen tier until a default snapshot repository is configured."
    }
  ]
}<p>The data stays on hot until a default repository is configured and exists. The same logic applies if the cluster lacks a valid Enterprise license. Errors are visible in the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-explain-data-lifecycle">data stream lifecycle status API</a> for that stream.</p><h3>Configuring frozen_after in Kibana</h3><p>Kibana in 9.5 lets you set <code>frozen_after</code> and the default snapshot repository from the UI. In <strong>Streams</strong>, the Retention tab shows the frozen phase on the lifecycle timeline alongside hot and any downsampling steps. Click the timeline to open the data lifecycle flyout, set <code>frozen_after</code>, and see the timeline update before you save. <strong>Index Management</strong>'s Data Streams page opens the same flyout.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt569984ebea197a38/6a7c38dff6ab871d74d15bb6/image1.jpg" alt="Kibana Streams UI showing frozen_after set to 30 days in the data stream lifecycle Edit data phases flyout" /><h2>How frozen tier conversion works in data stream lifecycle</h2><p>When a backing index ages past <code>frozen_after</code>, DLM walks through five steps in order:</p><ol><li><p><strong>Clone</strong>. Mark the index read-only and clone it to a zero-replica copy so the original stays available while conversion runs.</p></li><li><p><strong>Force merge</strong>. Merge the clone to a single segment. On completion a cluster state marker is written; duplicate force-merge requests (for instance after a master failover) are deduplicated, so a restart doesn't repeat the work.</p></li><li><p><strong>Snapshot</strong>. Write the merged clone to the default repository, and record the snapshot name in cluster state on success. If a stalled snapshot from a previous attempt is detected, DLM deletes it and re-runs the step.</p></li><li><p><strong>Mount</strong>. Create a partially-mounted searchable snapshot index from the snapshot.</p></li><li><p><strong>Swap and delete</strong>.Once the mounted index's shards are fully allocated, atomically swap it in for the original in the data stream, then delete the original. The swap is atomic, so query results don't see a data volume dip during the transition.</p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt35ff5577f138d9de/6a7c38fb1012c95976266824/image2.png" alt="Data stream lifecycle frozen tier conversion steps: clone, force merge, snapshot, mount, swap and delete" /><p>On failure, DLM retries from the last successful step on the next run.</p><p>Each step is idempotent, and the cluster state markers make sure work already done isn't repeated after a master failover. Throttling caps concurrent conversions so a lifecycle change covering thousands of indices doesn't overwhelm the cluster. Errors surface in the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-explain-data-lifecycle">data stream lifecycle status API</a> and roll up to the lifecycle health indicator.</p><h2>Scope and limitations of frozen_after</h2><ul><li><p><strong>Data indices only</strong>. <code>frozen_after</code> applies to a data stream's data indices. Failure store indices don't currently support the frozen tier and continue to be governed by their own <code>data_retention</code> setting.</p></li><li><p><strong>Enterprise license required</strong>. Frozen tier in DLM is implemented with searchable snapshots and requires an Enterprise license. You can write <code>frozen_after</code> on any license, but data won't move to frozen until the license is valid.</p></li><li><p><strong>Serverless ignores the field</strong>. In Elastic Cloud Serverless, <code>frozen_after</code> is accepted but ignored - Serverless manages tiering on your behalf. Built-in templates may include the field, so we don't reject it, but the step is skipped.</p></li></ul><h2>Getting started with frozen_after</h2><ol><li><p>In Kibana, open <strong>Streams</strong> or <strong>Index Management</strong> and choose a data stream backed by data stream lifecycle.</p></li><li><p>Open the <strong>Edit data lifecycle</strong> flyout, set a frozen-after value, and save. The lifecycle timeline shows the new phase.</p></li><li><p>On a self-managed cluster, set <code>repositories.default_repository</code> to a repository you control. On ECH, <code>found-snapshots</code> is already configured if you want zero setup.</p></li><li><p>For declarative workflows, write the same configuration into your index templates so new data streams pick up the lifecycle automatically.</p></li></ol><h2>Learn more</h2><ul><li><p><a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/data-stream-lifecycle">Data stream lifecycle</a></p></li><li><p><a href="https://www.elastic.co/docs/manage-data/lifecycle/data-tiers#frozen-tier">Frozen tier overview</a></p></li><li><p><a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">Searchable snapshots</a></p></li><li><p><a href="https://www.elastic.co/docs/solutions/observability/streams/management/retention">Manage data retention for Streams</a></p></li></ul><p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/data-stream-lifecycle-frozen-tier</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/data-stream-lifecycle-frozen-tier</guid>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Edward Lewis]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a5176d6139aa2d7/6a7c38c9be33783da7dadeb9/elastic-de_150810_blogheaderimage_ciscorevolutionizesai_treated_02_V1.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One field, every modality: how Elasticsearch's semantic field indexes and searches images, audio, video and PDFs automatically]]></title>
    <description><![CDATA[The semantic field turns images, audio, video, PDFs and text into multimodal embeddings at ingest time. Describe a scene and find the matching image or use a video frame to surface related clips, all from one Elasticsearch field.]]></description>
    <content:encoded><![CDATA[<p>Multimodal search in Elasticsearch now works the same way text search does: define a field, index your content, and query. The <code>semantic</code> field generates embeddings automatically at ingest time for images, audio, video, and PDFs. Every modality lands in one shared vector space, so you can retrieve an image with a text description, match audio to a phrase, or find a video with a still frame, all from a single field. Available in Elasticsearch 9.5 and serverless as a tech preview.</p><h2>The palette takes shape: how multimodal search in Elasticsearch evolved from semantic_text</h2><p>The <code>semantic</code> field is a convergence of several complementary features we've introduced over the past couple of years, bringing them together to create a cohesive multimodal search experience. Each solved an important piece of the semantic search puzzle on its own; together they enable native multimodal search.</p><p>The first brushstroke was <code>semantic_text</code>. Before it, running semantic search meant manually configuring mappings, wiring up ingest pipelines with an ML model, manually chunking content, and generating query-time embeddings yourself. The <code>semantic_text</code> field folds all of that away: it performs inference automatically at ingest time, chunks long documents for you, and simplifies the queries you write against it. <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Introduced in Elasticsearch 8.15</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-semantic-text-ga">released as GA in Elasticsearch 8.18</a>, it has become the foundation for semantic search on the platform.</p><p>Next came <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-omni-all-media-one-index">the model to power multimodal search</a>. <code>jina-embeddings-v5-omni</code> is our family of multimodal embedding models, capable of embedding text, images, video, audio, and PDFs into a shared vector space. Because those embeddings are semantically compatible across modalities, you can store diverse media in a single index and query across all of it at once, such as retrieving an image via a text description or matching audio against a written phrase, all without maintaining a separate pipeline for each content type. For more detailed information about how these embeddings are generated, see the <a href="https://jina.ai/models/jina-embeddings-v5-omni-small/">model documentation</a>.</p><p>We added the <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#query-vector-builders-parameters">embedding query vector builder</a> in Elasticsearch 9.4 to handle multimodal inputs at query time. Query vector builders are general-purpose tools you can use to convert input to a vector at query time as part of your request. For example, we have the <code>text_embedding</code> query vector builder for text-only models and input, and the <code>lookup</code> query vector builder for getting a vector from an existing document. The <code>embedding</code> query vector builder is a new type that works with multimodal models and accepts multimodal input, including text or base64-encoded binaries. This allows you to pose a query in whatever modality fits, and Elasticsearch generates the matching vector on the fly.</p><p>The final piece was multimodal ingest. The <code>semantic_text</code> field brought automatic embedding to text; the <code>semantic</code> field extends that same automatic experience to images, audio, video, and PDFs from ingest through query.</p><h2>Painting the picture: creating an index with the semantic field</h2><p>Let’s create an index with a <code>semantic</code> field. This is as simple as setting the field type to semantic and defining the inference endpoint you want to use:</p>PUT example-index
{
  "mappings": {
    "properties": {
      "my_semantic_field": {
        "type": "semantic",
        "inference_id": ".jina-embeddings-v5-omni-small"
      }
    }
  }
}<p>In this example, we use the .<code>jina-embeddings-v5-omni-small</code> inference endpoint. This is our built-in <code>jina-embeddings-v5-omni</code> inference service, and it is available in all environments with access to the <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). This includes:</p><ul><li><p>Serverless.</p></li><li><p>Elastic Cloud Hosted (ECH).</p></li><li><p>Self-managed with <a href="https://www.elastic.co/docs/deploy-manage/cloud-connect">Cloud Connected Mode</a> (CCM).</p></li></ul><h3>Indexing images, audio, video and PDFs</h3><p>To index an image, provide an object with a <code>type</code> of <code>image</code> and a <code>value</code> containing the image as a base64-encoded <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data">data URL</a>:</p>PUT example-index/_doc/example_doc_1
{
  "my_semantic_field": {
    "type": "image",
    "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
  }
}<p>Arrays of objects are also accepted, allowing you to index multiple images in a single field value:</p>PUT example-index/_doc/example_doc_2
{
  "my_semantic_field": [
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    },
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    }
  ]
}<p>The <code>semantic</code> field also supports text values, just like <code>semantic_text</code>. You can provide such values standalone or intermix them with image values:</p>PUT example-index/_doc/example_doc_3
{
  "my_semantic_field": "a cat on a windowsill"                                                                                                                                                                                                                }

PUT example-index/_doc/example_doc_4
{
  "my_semantic_field": [
    "a cat on a windowsill",
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    },
    "a dog running in a park"
  ]
}<p>Text values are handled just like they are with <code>semantic_text</code>: long passages are chunked according to the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-field-reference#semantic-params">chunking settings configured on either the inference service or field mapping</a>. Multimodal values, such as images, are not chunked. Each multimodal value is represented as one chunk.</p><p>Other modalities are supported as well. Change the type value to match your content’s modality. Currently we support:</p><ul><li><p><code>image</code></p></li><li><p><code>audio</code></p></li><li><p><code>video</code></p></li><li><p><code>pdf</code></p></li></ul><p>For example, to index a video, the request would look like:</p>PUT example-index/_doc/example_doc_5
{
  "my_semantic_field": {
    "type": "video",
    "value": "data:video/mp4;base64,&lt;base64-encoded-video-bytes&gt;"
  }
}<p></p><h3>Image search and cross-modal retrieval with a text query</h3><p>To find multimodal content using a text description, run a <code>match</code> query on the <code>semantic</code> field:</p>GET example-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>Just like with <code>semantic_text</code>, Elasticsearch automatically generates an embedding for the query text using the inference endpoint associated with the field. That query embedding is used to return semantically similar matches.</p><p>This query pattern enables easy text-to-image search. Just index an image and use a <code>match</code> query to retrieve it via text description! It also works for any other modality: index the multimodal input and search by description to retrieve it.</p><h3>Querying with images, video, and other multimodal inputs</h3><p>We can also search using a multimodal input by using the <code>knn</code> query with an <code>embedding</code> query vector builder. For example, we can search using an image:</p>GET example-index/_search
{
  "query": {
    "knn": {
      "field": "my_semantic_field",
      "query_vector_builder": {
        "embedding": {
          "input": {
            "type": "image",
            "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
          }
        }
      }
    }
  }
}<p>The <code>input</code> object format is the same as when providing an image to index: set the <code>type</code> to <code>image</code> and <code>value</code> to a base64-encoded data URL.</p><p>Similar to when querying by text description, Elasticsearch automatically generates an embedding for the query image using the inference endpoint associated with the field. That query embedding is used to return semantically similar matches.</p><p>Just like with indexing, other modalities are supported, but are limited to those supported by your inference endpoint. For example, a search using a video clip would look like:</p>GET example-index/_search
{
  "query": {
    "knn": {
      "field": "my_semantic_field",
      "query_vector_builder": {
        "embedding": {
          "input": {
            "type": "video",
            "value": "data:video/mp4;base64,&lt;base64-encoded-video-bytes&gt;"
          }
        }
      }
    }
  }
}<h2>Extending the composition: highlighting, retrievers, and other semantic field features</h2><p>The <code>semantic</code> field didn't start from a blank canvas. It's built on the same foundation as <code>semantic_text</code>, inheriting its behavior and its ergonomics, and extending them to multimodal content. In practice, that means nearly everything you already know about working with <code>semantic_text</code> carries over unchanged. If you've built with <code>semantic_text</code> before, the <code>semantic</code> field will feel immediately familiar.</p><p>Here’s a selection of the features that come along for the ride. See <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-field">the documentation</a> for a complete list.</p><h3>Highlighting the best-matching chunks</h3><p>If you index multiple values in a <code>semantic</code> field, you may want to know <em>which</em> value best matches the query. The <code>semantic</code> highlighter can be used to return the most relevant chunks as highlight fragments:</p>GET example-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  },
  "highlight": {
    "fields": {
      "my_semantic_field": {
        "number_of_fragments": 2,
        "order": "score"
      }
    }
  }
}<p>Setting <code>order</code> to <code>score</code> returns the fragments ranked by relevance, while <code>number_of_fragments</code> caps how many chunks come back. The response looks like:</p>{
  "hits": {
    "hits": [
      {
        "_index": "example-index",
        "_id": "example_doc_4",
        "_source": {...},
        "highlight": {
          "my_semantic_field": [
            "a cat on a windowsill",
            "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
          ]
        }
      }
    ]
  }
}<p>Note how highlighted multimodal values are represented using their data URLs.</p><h3>Controlling vector quantisation with index options</h3><p>The <code>semantic</code> field stores its embeddings in an underlying vector field, and <code>index_options</code> lets you control how that vector field is indexed. For example, choosing a non-default quantization strategy:</p>PUT example-index
{
  "mappings": {
    "properties": {
      "my_semantic_field": {
        "type": "semantic",
        "inference_id": ".jina-embeddings-v5-omni-small",
        "index_options": {
          "dense_vector": {
            "type": "int8_hnsw"
          }
        }
      }
    }
  }
}<h3>Multi-field retrievers</h3><p>The <code>semantic</code> field participates in the <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers">multi-field query format</a> supported by the <code>linear</code> and <code>rrf</code> retrievers. Rather than hand-writing an inner retriever per field, you supply a single <code>query</code> and a list of <code>fields</code>, mixing lexical fields and semantic fields freely:</p>GET example-index/_search
{
  "retriever": {
    "linear": {
      "query": "a cat on a windowsill",
      "fields": ["title", "my_semantic_field"],
      "normalizer": "minmax"
    }
  }
}<p>The retriever automatically separates lexical fields from semantic fields, queries each group, and normalizes the results so that each group contributes equally to the final ranking, preventing lexical matches from drowning out semantic ones.</p><h3>Cross-cluster search</h3><p>The <code>semantic</code> field supports <a href="https://www.elastic.co/docs/solutions/search/cross-cluster-search">cross-cluster search (CCS)</a>, enabling use of the field in large, multi-cluster deployments. Simply list the indices to query using the standard <code>&lt;cluster&gt;:&lt;index&gt;</code> format:</p>GET example-index,remote-cluster:remote-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>The fields queried across indices and clusters can use a mix of different inference endpoints that produce different query embeddings. The search request will automatically apply the proper query embedding to each individual field queried.</p><h2>Off the easel, into the world: optimising multimodal embeddings for production</h2><p>When you move multimodal search from experiment to production, the size of your multimodal inputs becomes a practical concern. Multimodal data is supplied as base64-encoded data URLs, and that data is stored in the index. Those strings can grow large in a hurry: a single high-resolution file can balloon into several megabytes of encoded text, which has several side effects:</p><ul><li><p>The index size on disk can increase significantly.</p></li><li><p>Requests and responses containing multimodal data are larger, increasing transmission time and ingress/egress costs.</p></li><li><p>Inference on larger multimodal inputs is slower.</p></li></ul><p>The good news is that you don’t need that much fidelity. Multimodal embedding models reduce each input to a compact representation before generating a vector anyway, so a smaller, lower-fidelity version of a multimodal input (such as a downscaled image or a lower-bitrate audio clip) produces a very similar embedding, and similar search quality, to its full-size original. This also applies to PDF input. PDFs are generally processed visually by multimodal models, so the quality only needs to be good enough to perform operations like image embedding and OCR. Long PDFs should be broken up into chunks of smaller inputs, so the embeddings generated more accurately represent each chunk. Feeding the model small inputs keeps your documents lean, trims index and response sizes, and speeds up ingestion, all without meaningfully affecting relevance. </p><p>Elasticsearch reinforces this practice with a guardrail: the <code>indices.inference.max_binary_input_size</code> cluster setting caps the size of each binary input, defaulting to 1 MB. Any individual value that exceeds the limit is rejected with a clear error, so oversized inputs surface as an actionable problem at index time rather than as silent bloat. This setting is adjustable in self-hosted and ECH through the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cluster-put-settings">cluster settings API</a>. It is not adjustable in our serverless offering, where 1 MB is the hard limit for binary sizes.</p><p>When possible, it is also advised to use <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#source-filtering">source filtering</a> to exclude <code>semantic</code> fields from responses. For example:</p>GET example-index/_search
{ 
  "_source": {
    "excludes": ["my_semantic_field"]
  },
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>This makes responses smaller, more performant, and easier to parse because multimodal data is not returned with each.</p><h2>Try out the semantic field</h2><p>The <code>semantic</code> field is available in Elasticsearch 9.5 and Serverless. <a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;cta=cloudregistration&amp;tech=trial&amp;plcmt=cross%20module&amp;pg=search-labs">Start a free trial</a> and try it out today.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-field-multimodal-search-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-field-multimodal-search-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[Mappings]]></category>
    <dc:creator><![CDATA[Mike Pellegrini]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac28de8857eafbc4/6a6f090aca9a724b3c614914/image1.png" length="0" type="image/png"/>
    <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Connectors: Performance impact of incremental syncs]]></title>
    <description><![CDATA[Learn about full sync and incremental sync for connectors. Discover how incremental sync can boost the performance of Elastic connectors.]]></description>
    <content:encoded><![CDATA[<h2>Elastic Connectors overview</h2><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors.html">Elastic Connectors</a> are a type of Elastic integrations that sync data from an original data source to an Elasticsearch index. Connectors enable you to create searchable, read-only replicas of your data sources.</p><p>There are a number of connectors that are supported for variety of 3rd-parties, such as:</p><ul><li><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-mongodb.html">MongoDB</a></p></li><li><p>Various SQL DBMS such as <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-mysql.html">MySQL</a>, <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-postgresql.html">PostgreSQL</a>, <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-ms-sql.html">MSSQL</a> and <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-oracle.html">OracleDB</a></p></li><li><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-sharepoint-online.html">Sharepoint Online</a></p></li><li><p><a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors-s3.html">Amazon S3</a></p></li><li><p>And many more. The full list is available <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors.html#connectors-build">here</a>.</p></li></ul><h2>Connectors content synchronization jobs</h2><p>Connectors support two types of content synchronization jobs: full syncs and incremental syncs.</p><h3>1. Full syncs</h3><p>Full sync is a sync that extracts all desired documents from a 3rd-party service and ingests them into Elasticsearch. So if you've set up your Network Drive connector to ingest all documents from a folder "\Documents/Reports\2022**.docx", during a full sync the connector will fetch all the documents that match this criteria and send all of them to Elasticsearch. Simplified pseudocode for this would look like:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

for incoming_document_metadata in connector.extract_documents():
    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    elasticsearch.ingest(document)
<p>This works well until the sync starts to take too long. This could happen because the connector fetches more data than needed. For instance, why fetch old files that have not changed and send them to Elasticsearch? One could argue that the metadata for files could be unreliable, so all files need to be fetched again and sent to Elasticsearch. Indeed, that could be the case, but if we can trust the metadata of the data fetched from 3rd-party, we can ingest less data. Incremental sync is the way to do so.</p><h3>2. Incremental syncs</h3><p>Most of the time, if written well, connector spends doing IO operations. Returning to the example code there are 3 places where IO happens:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

# Place #1: reading document metadata from 3rd-party system
for incoming_document_metadata in connector.extract_documents():
    # Place #2: reading document content from 3rd-party system
    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    # Place #3: ingesting the resulting document into Elasticsearch
    elasticsearch.ingest(document)
<p>Each of these places can become a bottleneck and take a significant amount of time during the sync.</p><p>Here's where incremental sync comes into play. Its purpose is to decrease the amount of IO on any of the stages, if possible.</p><h2>Potential optimizations for incremental sync</h2><h3>Fetch fewer documents from 3rd-party systems</h3><p>Modifying the example above, the code could look like this:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

# We can store last sync time somewhere
last_sync_time = connector.fetch_last_sync_time()

# And later use it querying Network Drive
for incoming_document_metadata in connector.extract_documents(from=last_sync_time):
    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    elasticsearch.ingest(document)
<p>In cases where only a small number of documents change in our 3rd-party system, we can speed up the ingestion process significantly. However, for Network Drive it's not possible - its API does not support filtering documents by metadata. We won't be able to avoid scanning through the full content of Network Drive.</p><h3>Skip download of content of files that haven't changed since previous sync</h3><p>Downloading file content takes a significant amount of time in the syncs. If files are reasonably large, the connection is unstable or throughput is low, downloading the content of files would take most of the time when syncing the content from the 3rd-party. If we skip downloading some of them, it can already significantly speed up the connector.</p><p>Consider the following example pseudocode:</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

last_sync_time = connector.fetch_last_sync_time()

for incoming_document_metadata in connector.extract_documents():
    # If document timestamp did not change then not fetching
    # document content can save us a lot of time
    if incoming_document_metadata["last_updated_at"] &gt; last_sync_time
        content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content
    }

    elasticsearch.ingest(document)
<p>If no documents were updated, the sync will actually be magnitudes faster than fully syncing the content.</p><h3>Skip ingestion of non-modified documents into Elasticsearch</h3><p>While it may seem minor, ingestion of data into Elasticsearch takes a significant amount of time - albeit normally less than downloading the content from the 3rd-party system. We can start storing timestamps per each document and not send the documents into Elasticsearch if their timestamp did not change.</p><p>We can combine this approach with the previous approach to save the most time possible during the sync.</p>connector = NetworkDriveConnector(
  host="192.168.0.105",
  path="\\Documents\Reports\2025\**.doc"
)

# We need to fetch only IDs and timestamps as it's sufficient to make a decision.
# For large indices it can still take a good amount of RAM, but that's the price.
existing_documents = connector.fetch_existing_documents(fields=["id", "_timestamp"])

for incoming_document_metadata in connector.extract_documents():
    existing_document_metadata = existing_documents[document_metadata["id"]]
    
    # If a document for this 3rd-party record exists in Elasticsearch index
    # and timestamp did not change, then skip downloading its content
    # and skip ingesting the document
    if existing_document_metadata:
        incoming_document_timestamp = incoming_document_metadata["last_updated_at"]
        existing_document_timestamp = existing_document_metadata["_timestamp"]

        if incoming_document_timestamp == existing_document_timestamp:
            # Skip the document for good
            continue;

    content = connector.download(incoming_document_metadata)

    document = {
      "id": incoming_document_metadata["id"]
      "content": content,
      "_timestamp" = incoming_document_metadata["last_updated_at"]
    }

    elasticsearch.ingest(document)
<p>This approach helps save even more time when running a sync. Now let's take a look into performance considerations for such improvements.</p><h2>Measuring incremental sync performance</h2><p>Now since we've taken a look into simplified code that shows how incremental syncs can work, we can try to estimate potential performance improvements.</p><p>For some connectors, incremental sync is implemented in a certain manner that optimizes the way data is fetched from a 3rd-party. For example, the Sharepoint Online connector fetches some data via delta API - only collecting documents that changed after the last sync. This improves performance in an obvious manner - less data -&gt; less time to sync the data to latest.</p><p>For other connectors (currently all connectors except Sharepoint Online connector) incremental sync is done by framework in a generic way which was described in one of previous sections <a href="https://www.elastic.co/search-labs/blog/elastic-connectors-performance-impact-of-incremental-syncs#skip-ingestion-of-non-modified-documents-into-elasticsearch">"Skip ingestion of non-modified documents into Elasticsearch"</a>.</p><p>Connectors still collect all the data from 3rd-party data source (as it does not provide a way to fetch only the changed records). However if this data contains timestamps, the connector framework compares document IDs and timestamps of already ingested documents with incoming documents. If the document exists in Elasticsearch with the same timestamp that was received from the 3rd-party data source, then this document will not be sent to Elasticsearch.</p><p>We've described abstract approach for the performance improvements with incremental syncs, but we already have these implemented in connectors, so let's dive into real numbers!</p><h3>Performance tests</h3><p>We will estimate the rough magnitude of improvement for incremental syncs with these performance tests, not aiming at high precision.</p><p>The two connectors chosen for this test, Google Drive and Github, were chosen because they have different IO profiles.</p><p>Google Drive acts like a file storage. It:</p><ul><li><p>Has a fast API that does not throttle too soon</p></li><li><p>Normally stores a lot of binary content of variable size - from small to really large</p></li><li><p>Normally stores a small number of records - tens or hundreds of thousands rather than millions</p></li></ul><p>GitHub data is ingested via a more of a classic API, that:</p><ul><li><p>Throttles quite often</p></li><li><p>Contains many records that are much smaller than those in Google Drive</p></li><li><p>Does not send binary content at all</p></li></ul><p>Due to these differences, the incremental sync performance will majorly differ.</p><p>Both tests will contain these mandatory steps:</p><ol><li><p>Do a full sync against a 3rd-party system</p></li><li><p>Modify some documents on the 3rd-party system</p></li><li><p>Run an incremental sync and check the amount of time it takes</p></li></ol><p>This setup is very bare bones but will already give a good indication of the magnitude of performance improvement. Both tests will be slightly different and I will provide results with commentary in the next section.</p><h4>Setup #1 - Google Drive Connector</h4><p>Initial setup will be:</p><ul><li><p>1 folder is on Google Drive with 1553 files (100 of them are 2MB in size, 1443 are 5KB in size)</p></li><li><p>A full sync is executed and this data gets into Elasticsearch</p></li><li><p>More files are added into Google Drive to make it 10144 files (100 of them are 2MB in size, all the rest are 5KB in size)</p></li><li><p>Incremental sync is executed again to pull the new data</p></li><li><p>Then some minor changes are made to files on Google Drive (1 added, 2 deleted)</p></li><li><p>Incremental sync is executed again</p></li><li><p>Full sync is executed to compare the run time against incremental sync again</p></li></ul><p>The following table contains the results of the described test with commentary:</p><p>Sync Description</p><p>Run time</p><p>Documents Added</p><p>Documents Deleted</p><p>Comment</p><p>Initial Full Sync</p><p>0h 4m 0s</p><p>1553</p><p>0</p><p>This is initial sync - it pulls all documents</p><p>Incremental Sync after more data was added to Google Drive</p><p>0h 20m 9s</p><p>7939</p><p>0</p><p>Run time was high as expected - a lot of documents went in</p><p>Incremental Sync after some data was slightly changed in Google Drive</p><p>0h 1m 25s</p><p>1</p><p>2</p><p>Run was very fast. It still called Google Drive API a lot, but did not have to ingest 200+MB of data into Elasticsearch</p><p>Full Sync to compare performance</p><p>0h 23m 23s</p><p>10144</p><p>0</p><p>As expected, it takes a lot of time - all the data is downloaded from Google Drive and is sent to Elasticsearch, even if it did not change. We can assume that it takes 22 minutes to download and then upload the data into Elasticsearch for the setup</p><p>In summary, incremental sync significantly improved the performance of the connector because most of the time is spent on the connector downloading the content of the files and sending this content to Elasticsearch. Full sync brings 2 * 100 + 1443 * 5 / 1024 = 207MB of content - both downloaded by connector and ingested into Elasticsearch. If only 1 large file is changed, this amount changes to only 2MB - a magnitude of 100 change. This explains the performance improvement well.</p><h4>Setup #2: GitHub connector</h4><p>The GitHub connector is very different since the actual volume of data it syncs is relatively small - issues, pull requests and such are reasonably small, while there are lots of them. Additionally, GitHub has strict throttling policies and throttles connector a lot.</p><p>To give a good real-world example we’ll use the Kibana Github repository with the GitHub connector and observe its performance.</p><p>Sync Description</p><p>Run time</p><p>Documents Added</p><p>Documents Deleted</p><p>Comment</p><p>Initial Full Sync</p><p>8h 40m 1s</p><p>147421</p><p>0</p><p>---</p><p>Incremental Sync ran immediately after</p><p>9h 6m 7s</p><p>59</p><p>0</p><p>This sync took even more time to run, mostly because it was constantly throttled. Connector had to fetch all the data from GitHub but sent only 59 records with a total volume of less than 1MB</p><p>Next incremental sync</p><p>9h 2m 52s</p><p>191</p><p>1</p><p>This sync was triggered immediately after previous incremental sync finished. Run time is the same due to data being almost the same and throttling being a major factor in the connector run time</p><h3>Key takeaways</h3><ul><li><p>As you can see, there is no performance improvement for incremental sync for the Github connector - there is barely any space for optimization as most of the time is spent by the connector querying the system and waiting for the throttling to stop.</p></li><li><p>Documents that are extracted are reasonably small, so network throughput usage is minimal. To improve the connector run time, the incremental sync would actually have to limit the number of queries to Github, but at this point it's not implemented in the connector.</p></li></ul><h2>Summary</h2><p>What is the primary factor that impacts the performance of incremental sync? In simplified terms, it's the raw volume of data that is ingested.</p><p>For Sharepoint Online connector there is a special logic to fetch less data via the <a href="https://learn.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0&amp;tabs=http">delta API</a>. This saves good amount of time because the delta API allows connectors to not fetch files that were not changed. Files tend to be large, thus not downloading and ingesting them will save a lot of time.</p><p>For other connectors, incremental sync is generic - it just checks document timestamps before ingesting them to Elasticsearch - if this document is already in the index and the timestamp did not change, then it is not ingested. It saves less time than the previous approach that Sharepoint Online employs but works generically for all connectors. Some connectors - ones that contain large documents - benefit from this logic a lot, while others - that get throttled by a 3rd-party system and contain relatively small documents - get no benefit from incremental syncs.</p><p>Additionally, if Elasticsearch is under heavy load, incremental sync is less likely to be throttled by Elasticsearch, thus making it more performant under load.</p><p>Let's look at the following graph:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc3a8743a67731c2f/6a171206dc55de4ca7e00f0d/b027636b0c73487b828a6c0390c808cc7da44420-1440x467.png" alt="" /><p>In the graph you can see how much time each part of content extraction and ingestion takes on the timeline. In the example above the connector is spending the most time on ingesting the data, even pausing for extraction and content download. In this case incremental sync has a potential of improving the run time of the sync by 30-40%.</p><p>Let's look at another example - a system that has throttling and low throughput, but stores very little data in Elasticsearch (Sharepoint Online, GitHub, Jira, Confluence):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaf686e760c07694/6a17120766c4f9a936f8c14b/c29df18ba0b125df44bf8e5895d910da85f9a585-1440x467.png" alt="" /><p>This system will not benefit from generic incremental syncs a lot - most of the time is spent extracting content from the 3rd-party system.</p><p>And the last example - fast and accessible system that stores huge amounts of data in Elasticsearch (Google Drive, Box, OneDrive, Network Drive):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd2fc28e350c89c02/6a1712092b835fa96ff4b333/524f5a5c1a547dc59d65fcdcb591a432dd8dfa11-1440x467.png" alt="" /><p>If there aren't too many items that change in such a system between syncs, this system will benefit a lot from generic incremental syncs.</p><p>Currently connectors that potentially get the most of incremental sync are:</p><ul><li><p>Azure Blob Storage</p></li><li><p>Box</p></li><li><p>Dropbox</p></li><li><p>Google Cloud Storage</p></li><li><p>Google Drive</p></li><li><p>Network Drive</p></li><li><p>OneDrive</p></li><li><p>S3</p></li><li><p>Sharepoint Online</p></li></ul><p>Other connectors will benefit less from incremental syncs, or will not benefit at all, but there's no one-size-fits-all answer here. Performance heavily depends on the profile of data ingested. The bigger each individual document is, the bigger the benefit.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-connectors-performance-impact-of-incremental-syncs</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-connectors-performance-impact-of-incremental-syncs</guid>
    <category><![CDATA[Index Data]]></category>
    <dc:creator><![CDATA[Artem Shelkovnikov]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc53d5b63416b4a59/6a17120ab0367df6a672be40/c5f9995397d0425d6e66399d4818a259bdeacc40-1280x611.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 27 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to detect which index template Elasticsearch will use before an index creation]]></title>
    <description><![CDATA[Learn about Elasticsearch index templates and how to detect which index template Elasticsearch will use before creating the index itself.]]></description>
    <content:encoded><![CDATA[<h2>Overview</h2><p>Elasticsearch offers two types of index templates: <code>legacy</code> and <code>composable</code>. Composable templates introduced in Elasticsearch 7.8 that are set to replace legacy templates, both can still be used in Elasticsearch 8.</p><p>This article explores the differences between these templates and how they interact. In particular, we will focus on how you can detect which template will be used when you are creating an index. Let's get started by looking at how to create the different types of index templates.</p><h2>Index templates in Elasticsearch</h2><p>Legacy templates can be created using the following API:</p>PUT _template/t1
{
  "order": 1,
  "index_patterns": [...],
  "mappings": {...},
  "settings": {...},
  "alias": {...}
}
<p>Composable templates can be created using this API:</p>PUT _index_template/ct1
{
  "priority": 1,
  "index_patterns": [...],
  "template": {
    "mappings": {...},
    "settings": {...},
    "alias": {...}
  }
}
<p>Component templates are a third type, which are typically used for managing multiple templates with similar structures. For example, if you need to create hundreds of templates with similar structures, you can create a component template with the common settings, mappings, and aliases, and then include it in your index templates. Component templates can be created using this API:</p>PUT _component_template/template_1
{
  "template": {
    "mappings": {...},
    "settings": {...},
    "alias": {...}
  }
}
<h3>Important!</h3><strong>When both legacy and composable templates exist and they match with the same index pattern, the legacy template will be ignored.</strong> If two composable templates point to the same index pattern, the template with the highest priority will be used. If two legacy templates point to the same index pattern, the templates are merged, with higher-order templates overriding lower-order ones. If the order is the same, the templates are sorted by name and merged accordingly.<h2>Determining which template an index will use when it is created</h2><p>To determine which template an index will use upon creation, you can use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-simulate-index.html"><code>_simulate_index</code></a> API. This API will return the template that will be used, along with any overlapping templates. However, if no composable templates are present, the API will return an empty body. In that case, you can create a dummy index and check the logs of the elected master node to determine which template will be used.</p><h2>What happens if you have both legacy templates and composable templates?</h2><p>As noted above, if you have both legacy and composable templates, the legacy template will be ignored as if it did not exist.</p>PUT _template/t1
{
  "index_patterns": [
    "test_index-*"
  ],
  "mappings": {
    "properties": {
      "field_1": {
        "type": "integer"
      },
      "field_2": {
        "type": "integer"
      }
    }
  }
}
<p>In such a case, you would get a warning message like the following when you run the command:</p>legacy template [t1] has index patterns [test_index-<em>] matching patterns from existing composable templates [ct1] with patterns (ct1 =&gt; [test_index-</em>]); this template [t1] may be ignored in favor of a composable template at index creation timePUT _index_template/ct1
{
  "index_patterns": [
    "test_index-*"
  ],
  "template": {
    "mappings": {
      "properties": {
        "field_1": {
          "type": "integer"
        }
      }
    },
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  }
}
<p>If a newly created composable template matches an existing legacy template with the same or includes an index pattern you will get a warning message like the following:</p>index template [ct1] has index patterns [test_index-<em>] matching patterns from existing older templates [t1] with patterns (t1 =&gt; [test_index-</em>]); this template [ct1] will take precedence during new index creationPOST _index_template/_simulate_index/test_index-1
#response:
{
  "template": {
    "settings": {
      "index": {
        "number_of_shards": "1",
        "number_of_replicas": "0",
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "field_1": {
          "type": "integer"
        }
      }
    },
    "aliases": {}
  },
  "overlapping": [
    {
      "name": "t1",
      "index_patterns": [
        "test_index-*"
      ]
    }
  ]
}
<p>Use this command if you want to test it:</p>PUT test_index-1
GET test_index-1
<h3>Notes from real life scenario</h3><p>Conflicts can be annoying, and they can crash the application. Imagine that you have <code>logstash-dev-*</code>, <code>logstash-prd-*</code>, <code>logstash-stg-*</code> legacy templates that all working fine. If someone adds a single composable template that include index pattern like a <code>logstash-*</code> all legacy templates will be ignored, the fields types can be change and finally it can break the application. Because of that, it’s recommended to switch from legacy to composable templates if you are using Elasticsearch 7 and onwards.</p><p>Another good point to keep in mind is that if you run the Logstash in Elasticsearch 8 or higher, Logstash will add it's template as composable template by default. Because <a href="https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html#plugins-outputs-elasticsearch-manage_template"><code>manage_template</code></a> is set to <code>true</code> by default and Logstash <a href="https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html#plugins-outputs-elasticsearch-template_api"><code>template_api</code></a> is set to<code>composable</code> for Elasticsearch 8 and onwards. It will create a Logstash composable template with <code>logstash-*</code> index pattern if the composable template does not exist. Yes, it will ignore all legacy templates covering <code>logstash-*</code> and overlap them.</p><p>Template Overlapping</p><h2>1. What happens if you have two composable templates that point to the same index pattern?</h2><p>As previously mentioned, if you have two composable templates that point to the same index pattern, the composable template with the highest priority will take precedence.</p>PUT _index_template/ct1
{
  "priority": 0,
  "index_patterns": [
    "test_index-*"
  ],
  "template": {
    "mappings": {
      "properties": {
        "field_1": {
          "type": "integer"
        }
      }
    },
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  }
}
PUT _index_template/ct2
{
  "priority": 1,
  "index_patterns": [
    "test_index-*"
  ],
  "template": {
    "mappings": {
      "properties": {
        "field_1": {
          "type": "keyword"
        },
        "field_2": {
          "type": "integer"
        }
      }
    },
    "settings": {
      "number_of_shards": 2,
      "number_of_replicas": 0
    }
  }
}
POST _index_template/_simulate_index/test_index-1
#response:
{
  "template": {
    "settings": {
      "index": {
        "number_of_shards": "2",
        "number_of_replicas": "0",
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "field_1": {
          "type": "keyword"
        },
        "field_2": {
          "type": "integer"
        }
      }
    },
    "aliases": {}
  },
  "overlapping": [
    {
      "name": "ct1",
      "index_patterns": [
        "test_index-*"
      ]
    }
  ]
}
<p>In this example, you have two templates—ct1 and ct2—both targeting the same index pattern test_index-<em>. However, ct2 has a higher priority (1) than ct1 (0). Therefore, when you create an index that matches the pattern test_index-</em>, the settings and mappings defined in ct2 will be applied before ct1. If there are the same settings in the ct1 and ct2 templates, the ct2 template will overwrite.</p><h2>2. What happens if you have two legacy templates that point to the same index pattern?</h2><p>As highlighted above, if you have multiple templates that point to the same index pattern, the templates with lower-order values are merged first. Templates with higher-order values are merged later, overriding templates with lower values.</p><p>If two legacy templates have the same order value, they will be sorted by name. For example, in a case with [t2, t1], t1 would be merged first, t2 would be merged later, and t2 would override t1 if there are any same mapping/settings/aliases.</p>PUT _template/t1
{
  "index_patterns": ["test_index-*"],
  "mappings": {
    "properties": {
      "field_1": {
        "type": "integer"
      }
    }
  },
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  }
}
PUT _template/t2
{
  "index_patterns": [
    "test_index-*"
  ],
  "mappings": {
    "properties": {
      "field_1": {
        "type": "geo_point"
      },
      "field_2": {
        "type": "long"
      }
    }
  },
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 0
  }
}
POST _index_template/_simulate_index/test_index-1
#response
{}
<p>Unfortunately, if you don't have composable templates, this API call responds with an empty body. So how you can check that?</p><p>The answer is to create a dummy index and check the Elasticsearch elected-master logs.</p>PUT test_index-test
2023-11-14 14:14:27 {"@timestamp":"2023-11-14T11:14:27.535Z", "log.level": "WARN",  "data_stream.dataset":"deprecation.elasticsearch","data_stream.namespace":"default","data_stream.type":"logs","elasticsearch.event.category":"templates","event.code":"index_template_multiple_match","message":"index [test_index-1] matches multiple legacy templates [t1, t2], composable templates will only match a single template" , "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"deprecation.elasticsearch","process.thread.name":"elasticsearch[elasticsearch][masterService#updateTask][T#3]","log.logger":"org.elasticsearch.deprecation.cluster.metadata.MetadataCreateIndexService","trace.id":"85e0a432ec11e2f2d3c7883f510376ac","elasticsearch.cluster.uuid":"Jc-a46VUSjOwuxWmbnSDZQ","elasticsearch.node.id":"MTX1x5-OTlWhiGa9lwUJPw","elasticsearch.node.name":"elasticsearch","elasticsearch.cluster.name":"elasticsearch-cluster1"}

2023-11-14 14:14:27 {"@timestamp":"2023-11-14T11:14:27.605Z", "log.level": "INFO", "message":"[test_index-1] creating index, cause [api], templates [t2, t1], shards [2]/[0]", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"elasticsearch[elasticsearch][masterService#updateTask][T#3]","log.logger":"org.elasticsearch.cluster.metadata.MetadataCreateIndexService","trace.id":"85e0a432ec11e2f2d3c7883f510376ac","elasticsearch.cluster.uuid":"Jc-a46VUSjOwuxWmbnSDZQ","elasticsearch.node.id":"MTX1x5-OTlWhiGa9lwUJPw","elasticsearch.node.name":"elasticsearch","elasticsearch.cluster.name":"elasticsearch-cluster1"}
<p>From the logs, we can see that "[test_index-1] creating index, cause [api], templates [t2, t1]".</p>GET _cat/templates/t*?v
name index_patterns order version composed_of
t2   [test_index-*] 0
t1   [test_index-*] 0
<p>As you can see, both legacy templates t1 and t2 have the same order; so, which one will override the other?</p><p>In this case, Elasticsearch will sort the legacy index templates according to their names and apply them. Both templates will be applied, and the first one in the list, which is t2 in this example, will override the template.</p><h4>Bonus: What happens if you have two legacy templates that point to the same index pattern with same field name but inappropriate type?</h4><p>Attempting to merge attributes within the legacy template, regardless of the order, is likely to fail since field definitions should remain atomic. This issue is a primary motivator for introducing the new composable templates. See the below example. We thank Philipp Krenn for adding these comments to the article.</p>PUT _template/test1
{
  "order": 3,
  "index_patterns": [
    "test-*"
  ],
  "mappings": {
    "properties": {
      "my_field": {
        "type": "integer",
        "ignore_malformed": true
      }
    }
  }
}
PUT _template/test2
{
  "order": 2,
  "index_patterns": [
    "test-*"
  ],
  "mappings": {
    "properties": {
      "my_field": {
        "type": "keyword",
        "ignore_above": 1024
      }
    }
  }
}
PUT test-1/_doc/1
{
  "my_field": "a string..."
}
#response:
{
  "error": {
    "root_cause": [
      {
        "type": "mapper_parsing_exception",
        "reason": "unknown parameter [ignore_above] on mapper [my_field] of type [integer]"
      }
    ],
    "type": "mapper_parsing_exception",
    "reason": "Failed to parse mapping: unknown parameter [ignore_above] on mapper [my_field] of type [integer]",
    "caused_by": {
      "type": "mapper_parsing_exception",
      "reason": "unknown parameter [ignore_above] on mapper [my_field] of type [integer]"
    }
  },
  "status": 400
}
<h2>Notes and good things to know</h2><ol><li><p>Using legacy templates in the same order can cause a lot of confusion. That’s why it's recommended to add order to the template.</p></li><li><p>Templates with lower-order values are merged first. Templates with higher order values are merged later, overriding templates with lower values.</p></li><li><p>You can't create two composable templates with the same priority.</p></li></ol>{
  "type": "illegal_argument_exception",
  "reason": "index template [ct2] has index patterns [test_index-*] matching patterns from existing templates [ct1] with patterns (ct1 =&gt; [test_index-*]) that have the same priority [0], multiple index templates may not match during index creation, please use a different priority"
}
<h2>Conclusion</h2><p>In conclusion, understanding how Elasticsearch's index templates work is crucial for effective index management. By knowing how to determine which template an index will use upon creation, you can ensure that your indices are created with the correct settings, mappings, and aliases.</p><h4>Resources</h4><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-template.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-template.html</a> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html</a> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-simulate-index.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-simulate-index.html</a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-index-template</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-index-template</guid>
    <category><![CDATA[Index Data]]></category>
    <dc:creator><![CDATA[Musab Dogan]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt298e839e708ca11c/6a170b0fb339d560c2769fc2/38bc0377a6adce7eae0099f61902fdbbe644eb4a-1440x960.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 09 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to ingest data from Snowflake to Elasticsearch]]></title>
    <description><![CDATA[Learn how to ingest data from Snowflake to Elasticsearch using Logstash or a Snowflake Elasticsearch Python Script.]]></description>
    <content:encoded><![CDATA[<p>To take advantage of the powerful search capabilities offered by Elasticsearch, many businesses keep a copy of searchable data in Elasticsearch. Elasticsearch is a scalable data store and vector database, proven for traditional text search and vector search in semantic search use cases. The Elasticsearch Relevance Engine</p><p>TM (ESRE) enables you to add semantic search on proprietary data that can be integrated with generative AI technologies to build modern search experiences.</p><p></p><p><a href="https://www.snowflake.com/">Snowflake</a> is a fully managed SaaS (software as a service) that provides a single platform for data warehousing, data lakes, data engineering, data science, data application development, and secure sharing and consumption of real-time/shared data.</p><p>In this blog, we will see how to bring your snowflake data to Elasticsearch using below methods:</p><ol><li><p>Using <a href="https://www.elastic.co/logstash">Logstash</a> (periodic sync)</p></li><li><p>Using <a href="https://github.com/ashishtiwari1993/snowflake-elasticsearch-connector">Snowflake Elasticsearch Python Script</a> (one time sync)</p></li></ol><h2>Prerequisites</h2><h3>Snowflake credentials</h3><p>You will have received all below credentials after <a href="https://signup.snowflake.com/">signup</a>, or you can get them from the Snowflake panel.</p><ul><li><p>Account username</p></li><li><p>Account password</p></li><li><p>Account Identifier</p></li></ul><h3>Elastic credentials</h3><ol><li><p>Visit <a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">https://cloud.elastic.co</a> and sign up.</p></li><li><p>Click on <strong>Create deployment</strong>. In the pop-up, you can change the settings or keep the default settings.</p></li><li><p>Download or copy the deployment credentials (both username and password).</p></li><li><p>Also copy the <a href="https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html">Cloud ID</a>.</p></li><li><p>Once you’re ready for deployment, click on <strong>Continue</strong> (or click on <strong>Open Kibana</strong>). It will redirect you to the Kibana dashboard.</p></li></ol><h2>Methods to ingest data from Snowflake to Elasticsearch</h2><h3>Method 1: Using Logstash</h3><p>Logstash is an open source ETL tool where you can provide multiple sources as an input, transform (modify) it, and push to your favorite stash. One of the famous use cases of Logstash is reading logs from the file and pushing to Elasticsearch. We can also modify the data on the fly using a <a href="https://www.elastic.co/guide/en/logstash/current/filter-plugins.html">filter</a> plugin, and it will push updated data to the output.</p><p>We’re going to use the <a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html">JDBC input plugin</a> to pull the data from Snowflake and push to Elasticsearch using the <a href="https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html">Elasticsearch output plugin</a>.</p><ol><li><p>Install Logstash by referring to the <a href="https://www.elastic.co/guide/en/logstash/current/installing-logstash.html">documentation</a>.</p></li><li><p>Go to the Maven Central Repository and download: <a href="https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc">https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc</a>.</p></li><li><p>Click on the directory for the version that you need and download the <strong>snowflake-jdbc-#.#.#.jar</strong> file. In my case, I have downloaded <code>snowflake-jdbc-3.9.2.jar</code>. (Refer to official documentation to learn more about the <a href="https://docs.snowflake.com/en/developer-guide/jdbc/jdbc">Snowflake JDBC Driver</a>.)</p></li><li><p>Create a pipeline by creating file <code>sf-es.conf</code>. Add the below snippet and replace all credentials.</p></li></ol>input {
  jdbc {
    jdbc_driver_library =&gt; "/usr/share/logstash/logstash_external_configs/driver/snowflake-jdbc-3.9.2.jar"
    jdbc_driver_class =&gt; "net.snowflake.client.jdbc.SnowflakeDriver"
    jdbc_connection_string =&gt; "jdbc:snowflake://&lt;account_identifier&gt;.snowflakecomputing.com/?db=SNOWFLAKE_SAMPLE_DATA&amp;warehouse=COMPUTE_WH&amp;schema=TPCH_SF1"
    jdbc_user =&gt; "&lt;snowflake_username&gt;"
    jdbc_password =&gt; "&lt;snowflake_password&gt;"
    schedule =&gt; "* * * * *"
    statement =&gt; "select * from customer limit 10;"
  }
}

filter {}

output {
  elasticsearch {
    cloud_id =&gt; "&lt;elastic cloud_id&gt;"
    cloud_auth =&gt; "&lt;elastic_username&gt;:&lt;elastic_password&gt;"
    index =&gt; "sf_customer"
  }
}
<p><strong>jdbc_connection_string</strong> :</p>db=SNOWFLAKE_SAMPLE_DATA
warehouse=COMPUTE_WH
schema=TPCH_SF1
<p><strong>Schedule:</strong> Here you can schedule to run this flow periodically using cron syntax. On every run, your data will be moved incrementally. You can check more on <a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html#_scheduling_2">scheduling</a>.</p><p>Please change according to your requirements.</p><p><a href="https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html#plugins-inputs-jdbc-jdbc_paging_enabled"><strong>JDBC Paging</strong></a> <strong>(Optional):</strong> This will cause a sql statement to be broken up into multiple queries. Each query will use limits and offsets to collectively retrieve the full result-set. You can use this to move all data in a single run.</p><p>Enable JDBC paging by adding below configurations:</p>jdbc_paging_enabled =&gt; true,
jdbc_paging_mode =&gt; "explicit",
jdbc_page_size =&gt; 100000

<ol><li><p>Run Logstash</p></li></ol>bin/logstash -f sf-es.conf
<h3>Method 2: Using Snowflake-Elasticsearch Python script</h3><p>If Logstash is not currently in place or has not been implemented, I have written a small Python utility, which is available <a href="https://github.com/ashishtiwari1993/snowflake-elasticsearch-connector">here on GitHub</a>, to pull data from Snowflake and push it to Elasticsearch. This will pull all your data at one time. So if you have a small amount of data to be migrated in a non-periodic manner, you can use this utility.</p><p><strong>Note:</strong> This is not a part of the official <a href="https://www.elastic.co/guide/en/enterprise-search/current/connectors.html">Elastic connectors</a>. Elastic connectors provide support for various data sources. You can use this connector if you have a requirement to sync data from any supported data sources.</p><ol><li><p>Installation</p></li></ol>git clone https://github.com/ashishtiwari1993/snowflake-elasticsearch-connector.git
cd snowflake-elasticsearch-connector
<ol><li><p>Installing dependencies</p></li></ol>pip install -r requirements.txt
<ol><li><p>Change configs</p></li></ol><ul><li><p>Open <code>config/connector.yml</code>.</p></li><li><p>Replace credentials with the following:</p></li></ul>snowflake:
  username: &lt;sf_username&gt;
  password: &lt;sf_password&gt;
  account: &lt;sf_account_identifier&gt;
  database: &lt;db_name&gt;
  table: &lt;table_name&gt;
  columns: ""
  warehouse: ""
  scheme: ""
  limit: 50

elasticsearch:
  host: https://localhost:9200
  username: elastic
  password: elastic@123
  ca_cert: /path/to/elasticsearch/config/certs/http_ca.crt
  index: &lt;sf_customer&gt;

<ol><li><p>Run connector</p></li></ol>python __main__.py
<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3056444664f99151/6a170b39ab7f08e6a0db9ea0/26faefb842a5d9af87d659c763adb79207980dc8-1475x582.gif" alt="Snowflake to Elasticsearch python script" /><h2>Verify data</h2><ol><li><p>Log in to Kibana and go to <strong>☰ &gt; Management &gt; Dev Tools</strong>.</p></li><li><p>Copy and paste the following API GET request into the Console pane, and then click the ▶ (play) button. This queries all records in the new index.</p></li></ol>GET sf_customer/_search
{
  "query": {
    "match_all": {}
  }
}

<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltceca98d9be170ff7/6a170b3b0c485744e701aa9a/e45399d16b795dbb936f95615e69590d2e6882dd-1440x697.png" alt="Output snowflake to elasticsearch" /><h2>Conclusion</h2><p>We have successfully migrated the data from Snowflake to Elastic Cloud. You can achieve the same on any Elasticsearch instance, whether it is in the cloud or on prem.</p><p>Start leveraging full text and <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html">semantic search capabilities</a> on your data set. You can also connect your data with LLMs to build <a href="https://www.elastic.co/search-labs/chatgpt-elasticsearch-openai-meets-private-data">Question - Answer</a> capabilities.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ingest-data-from-snowflake-to-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ingest-data-from-snowflake-to-elasticsearch</guid>
    <category><![CDATA[Index Data]]></category>
    <dc:creator><![CDATA[Ashish Tiwari]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac4b4ee213b421df/6a170b3c2867145fc693e31f/863d959e4481788dac10ed6abad63de2e823f2d0-1440x810.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 20 Dec 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>