Skip to content

Elasticsearch vs OpenSearch: Indexing Benchmarks

In the fastest configuration I found for each, Elasticsearch indexed 10 million documents to one searchable segment sooner than OpenSearch on both corpora. OpenSearch took 2% to 11% longer on Wikipedia full text and 7% to 10% longer on a structured faceting corpus, and used 10% to 16% more server CPU time. Elasticsearch’s lead comes before the force merge: it accepted the documents and made them searchable sooner, and the force merge to one segment that followed took the two engines about the same time.

With each engine’s shipped settings, the order reverses. Elasticsearch took 11% longer than OpenSearch on the facet corpus, 70.4 seconds against 63.5, and 2% longer on full text, inside the spread of the runs. The configuration changes were worth more to Elasticsearch: its shipped settings were 10% and 30% slower than its fastest configuration, against 5% and 9% for OpenSearch.

Elasticsearch’s full-text runs also varied more from run to run, enough that a single run of each engine could put either one ahead.

These measurements are part of a full comparison including Luxir, the search engine I develop. This page focuses on Elasticsearch and OpenSearch, using the same runs.

Results as of 2026-11-01
Elasticsearch 9.7.0-SNAPSHOT, Elastic’s official snapshot build of main at 87a91d32c (built 2026-10-30), Lucene 10.5.2, bundled JDK (OpenJDK 27), 20 GiB heap
OpenSearch 3.10.0 nightly build 12323, the official CI build of main at b86aeacb4 (built 2026-10-31), Lucene 10.5.2, bundled JDK (Eclipse Temurin 25), 20 GiB heap
Corpora 10 million Wikipedia documents (13.6 GB of JSON), and 10 million structured documents for faceting (4.0 GB); one node, one shard, no replicas
Hardware munin: AMD Ryzen 9 9955HX (16 cores, 32 threads), 64 GB RAM. 14 cores for the server, 2 for the feeder
Storage Every index in RAM (tmpfs), so neither engine waits on a disk
Runs Ten runs of each engine at each point, three with shipped settings, each on a freshly started server and an empty index. Every number is the mean
SearchBench luxir-search/searchbench, measured at the tag indexing-munin-20261101 on my fork. The full-text runs used the commit before it, which differs only in the bulk feeder
Raw results Release indexing-munin-20261101: every run’s record, including the configurations tried and the runs superseded, the generated tables, and the campaign driver, with a SHA-256 checksum

Both builds are unreleased official builds of their main branches from the same week.

Seconds to index 10 million Wikipedia documents to one searchable segment, at 1, 2, and 4 GiB of indexing RAM in each engine’s fastest configuration found: Elasticsearch 177.3, 190.2, and 181.2 seconds; OpenSearch 196.4, 193.8, and 195.5. With shipped settings at 2 GiB: Elasticsearch 208.6, OpenSearch 203.8. Each bar is split where every document became searchable: 66 to 74 seconds for Elasticsearch and 75 to 79 for OpenSearch.

Seconds to index the 10 million document facet corpus to one searchable segment, at 1, 2, and 4 GiB of indexing RAM in each engine’s fastest configuration found: Elasticsearch 54.7, 54.1, and 53.8 seconds; OpenSearch 58.3, 58.1, and 59.3. With shipped settings at 2 GiB: Elasticsearch 70.4, OpenSearch 63.5. Each bar is split where every document became searchable: about 24 seconds for Elasticsearch and 26 to 28 for OpenSearch.

The solid part of each bar is the time until every document was searchable. The hatched part is the force merge to one segment that followed. The last group in each chart is the two engines with their shipped settings.

Seconds from the first document sent, the mean of ten runs. ± is the standard deviation of the ten. Force merge is the time from visible to one segment, including a _flush before it. Server CPU-seconds run from the first document to one segment. Index is the data directory at one segment.

RAM Engine Acknowledged Visible Force merge One segment Server CPU-seconds Index
1 GiB Elasticsearch 60.6 s 65.7 s 111.5 s 177.3 s ± 10.9 1,746 10.59 GiB
1 GiB OpenSearch 71.4 s 74.5 s 121.9 s 196.4 s ± 6.4 1,928 10.58 GiB
2 GiB Elasticsearch 58.7 s 66.2 s 124.0 s 190.2 s ± 7.5 1,738 10.58 GiB
2 GiB OpenSearch 71.4 s 74.6 s 119.2 s 193.8 s ± 5.2 1,926 10.59 GiB
4 GiB Elasticsearch 55.4 s 69.2 s 111.9 s 181.2 s ± 16.3 1,666 10.59 GiB
4 GiB OpenSearch 71.9 s 75.7 s 119.8 s 195.5 s ± 4.8 1,938 10.59 GiB
RAM Engine Acknowledged Visible Force merge One segment Server CPU-seconds Index
1 GiB Elasticsearch 23.2 s 24.2 s 30.5 s 54.7 s ± 1.4 547 1.96 GiB
1 GiB OpenSearch 25.5 s 26.8 s 31.5 s 58.3 s ± 2.3 629 1.95 GiB
2 GiB Elasticsearch 23.1 s 24.0 s 30.0 s 54.1 s ± 1.4 545 1.96 GiB
2 GiB OpenSearch 25.4 s 27.8 s 30.2 s 58.1 s ± 2.2 624 1.95 GiB
4 GiB Elasticsearch 23.0 s 23.9 s 29.9 s 53.8 s ± 1.3 545 1.96 GiB
4 GiB OpenSearch 25.6 s 26.9 s 32.5 s 59.3 s ± 1.1 631 1.95 GiB

The two engines build indexes of the same size, with the same Lucene fields (see what each engine indexes).

Until every document is visible, both engines keep most of the server busy: 22 to 25 of its 28 hardware threads on full text, and 21 to 22 on the facet corpus. So the time to visible follows the CPU work. To get there, OpenSearch spent 10% to 17% more CPU time than Elasticsearch on full text and 16% to 17% more on the facet corpus, and its documents were visible 9% to 13% and 11% to 16% later.

The force merge is different. Both engines kept about one hardware thread busy through it, and it took them about the same time: within 9% of each other at every level, with OpenSearch the quicker one at 2 GiB on full text. It is also most of the run: 61% to 65% of the time to one segment on full text, and 52% to 56% on the facet corpus. A Lucene merge runs on a single thread, except for vector graphs, which neither corpus has, and the last merge, the one that produces the single segment, has nothing to run beside it. The full comparison shows what running that step in parallel is worth.

On full text, Elasticsearch accepted the documents sooner with more indexing RAM, from 60.6 seconds at 1 GiB to 55.4 at 4 GiB, without making them searchable any sooner: 65.7 to 69.2 seconds. Its _refresh after the last document returned within 2 seconds in 9 of its 30 runs, and took 5.9 to 20.1 seconds in the other 21: 6 to 11 seconds at 1 and 2 GiB, and 19 to 20 at 4 GiB. OpenSearch’s took 0.1 to 8.6 seconds, 3.2 to 3.8 on average. On the facet corpus, both took 0.1 to 4.6 seconds.

Elasticsearch’s full-text runs varied the most. Its time to one segment ranged from 157.9 to 201.7 seconds at 1 GiB, 183.1 to 204.3 at 2 GiB, and 164.9 to 218.0 at 4 GiB. OpenSearch’s ranged from 188.5 to 207.0, 187.3 to 200.5, and 189.3 to 203.8. Both phases contribute: the _refresh above, and the force merge, which took Elasticsearch 98.6 to 143.3 seconds across its 30 full-text runs and OpenSearch 112.1 to 135.6.

Pairing each Elasticsearch run with each OpenSearch run at the same level, Elasticsearch was faster in 92, 71, and 82 of the 100 pairs at 1, 2, and 4 GiB on full text, and in 91, 94, and 100 on the facet corpus. On full text, a single run of each can come out either way. The two engines’ runs were interleaved, so a drift in the machine over the campaign would have affected both alike.

Elasticsearch and OpenSearch with their own index settings, at 2 GiB, three runs each. Only the mapping, one shard, and no replicas are set, so the refresh interval, merge auto-throttling, the translog, and merge threads are all at each engine’s defaults. The nodes are the same as above: 20 GiB heap, a 2 GiB indexing buffer, security off.

Corpus Engine Visible Force merge One segment Against its fastest configuration
Full text Elasticsearch 74.3 s 134.4 s 208.6 s ± 8.7 10% slower
Full text OpenSearch 79.2 s 124.6 s 203.8 s ± 7.5 5% slower
Facet Elasticsearch 23.6 s 46.8 s 70.4 s ± 2.1 30% slower
Facet OpenSearch 26.4 s 37.1 s 63.5 s ± 5.0 9% slower

With shipped settings Elasticsearch still made the documents searchable first on both corpora, and lost the lead in the force merge: 134.4 seconds against 124.6 on full text, and 46.8 against 37.1 on the facet corpus, where its fastest configuration took 30.0. The full-text runs were fed by the earlier feeder, as were the full-text results above (see Feeding).

Before the measured runs, I tried a set of settings on each engine at 2 GiB of indexing RAM and kept the fastest to one segment for each corpus. Each configuration starts from a bulk-load posture (refresh off during the feed, merge auto-throttling off) and changes one or two settings. A change was kept only if it beat the posture by more than 5%. Seconds to one segment, two runs each on the facet corpus and one on full text; the configuration used for each corpus is in bold.

Configuration Facet Full text
Elasticsearch, bulk-load posture 78.2 192.0
Elasticsearch, 1 s refresh 54.5 212.6
Elasticsearch, 512 MB translog flush threshold 56.8 214.0
OpenSearch, bulk-load posture 60.9 199.7
OpenSearch, 14 merge threads 57.7 206.7
OpenSearch, 1 s refresh 57.5 255.4
OpenSearch, 1 s refresh and 14 merge threads 59.9
OpenSearch, 10 GB translog flush threshold 200.4
OpenSearch, 10 GB translog flush threshold and 14 merge threads 195.5

The bulk-load posture was Elasticsearch’s fastest configuration on full text and its slowest on the facet corpus. There, Elasticsearch accepted all the documents in 20.5 seconds, sooner than in any other configuration, but the _refresh that made them searchable then took 15.6 and 16.5 seconds in the two runs. With a 1 s refresh, the documents were all accepted at 22.8 seconds and searchable 1.6 seconds later, and the force merge was shorter too (30.2 seconds against 41.7). After the same feed in the posture, OpenSearch’s _refresh took 0.2 and 3.9 seconds.

OpenSearch’s two facet changes tie. 14 merge threads also won an earlier one-run pilot (58.9 s against 61.9). On full text, OpenSearch with a 10 GB translog threshold and 14 merge threads was 2% faster than the posture, under the 5% bar. OpenSearch’s defaults on this machine are 4 merge threads and a 512 MB translog threshold.

Full text. The first 10,000,000 documents of the English Wikipedia line-document file that Lucene’s own benchmark suite, luceneutil, uses: Wikipedia from May 2012, split into chunks of at most 1,024 characters. Each document also carries the fields of the facet corpus below. It is the corpus of the full-text search benchmark.

Facet. 10,000,000 documents without the text: an id, nine string fields with 10 to 5 million distinct values, seven string fields marking nested subsets from 99% down to 0.01% of the documents, two integer fields, and a date. It is the corpus of the string faceting benchmark, and indexing it is mostly about building per-field structures, not about text analysis.

Both engines get the same mapping: id and the string fields as keyword, body (full text only) as text with the standard analyzer, and the numbers as long, integer, and date with index: false and a doc values skip index, turned on with index.mapping.use_doc_values_skipper: true (Elasticsearch) or skip_list: true (OpenSearch). Neither builds a points index on the numeric fields.

To check that the two build the same thing, I read each engine’s finished segment with Lucene’s own API, using that engine’s jars. For every field, the internal metadata fields included, they record the same index options, doc values type, skip index, points, and norms. In both, the only field with points is _seq_no, the engine’s own per-document sequence number. Both keep _source, the whole original JSON of every document.

Each run starts a fresh server on an empty data directory, creates the index, and then times, from the first document sent:

  • Acknowledged: every _bulk request has been answered.
  • Visible: a _refresh sent right after has returned, so every document is searchable. Acknowledged documents are already durable in the transaction log, so a Lucene commit is not needed to make them searchable.
  • One segment: a _flush, then _forcemerge?max_num_segments=1, then a _refresh, have returned. The _flush took under a second in every run. Without it, Elasticsearch’s full-text runs often took this 64 GB machine below 2 GB of available memory near the end of the force merge, with every index held in RAM, and the harness stopped them.

Ending at one segment gives both engines the same finish line. “Done indexing” otherwise depends on where each merge policy happens to stop, and a single segment is how SearchBench serves its query benchmarks. After each run the harness checks that the index holds all 10,000,000 documents, live, in exactly one segment. The server’s CPU time comes from the kernel’s accounting for the server process.

Both engines take 32 concurrent _bulk connections, each a kept-alive connection sending 1,000 documents per request and waiting for the response before sending the next. With 64 or 128 connections, both were no faster on the facet corpus. The document ids are generated by the engine, which lets both skip the check for an existing document. The feeder is SearchBench’s own and runs on the two cores (four threads) set aside from the server.

An earlier version of the feeder opened a new connection for every request and parsed every response in full. On the facet corpus that held both engines back by about 16% while they were still feeding, and put OpenSearch ahead, so the facet runs were all redone with the current feeder. The full-text runs are the earlier feeder’s: there the servers are the limit, and single runs with the current feeder came out inside the ranges above (201.5 s for Elasticsearch and 191.5 s for OpenSearch, at 2 GiB).

  • Indexing RAM: indices.memory.index_buffer_size, set as an absolute size: 1, 2, or 4 GiB. It made little difference to either engine.
  • Heap: 20 GiB, five times the largest indexing buffer, on each engine’s bundled JDK. Elasticsearch ships Oracle’s build of OpenJDK 27 (27.0.1.0.3+1-2) and OpenSearch ships Eclipse Temurin 25.0.5 (25.0.5+8-LTS).
  • Merge auto-throttling off (index.merge.scheduler.auto_throttle: false) in every configuration except the shipped settings.
  • One shard, no replicas. Security off on both, and machine learning off on Elasticsearch.
  • Not changed: thread pools, the garbage collector, JVM options other than the heap size, and translog durability (a sync per request, which costs next to nothing on tmpfs).

The full comparison lists everything that was done to make both engines fast.

  • In RAM. On a real disk, write bandwidth and sync costs would add to both engines’ times.
  • One machine, one node, one shard, append-only. Updates and deletes, replicas, and multiple shards are not measured here.
  • Each engine’s fastest configuration found, per corpus, from a short list of settings and few runs per setting. A setting not tried could change the order.
  • Different JDKs. Each engine runs the JDK it ships with, which is part of what is being compared.