Repository navigation
Add bulk ingester - #3358
Open
rursprung wants to merge 6 commits into
Open
Add bulk ingester#3358rursprung wants to merge 6 commits into
rursprung wants to merge 6 commits into
Conversation
Contributor
Author
|
CC @sothawo, @reta, @siddeshbb & @patpatpat123 |
rursprung
force-pushed
the
add-bulk-ingester
branch
from
October 26, 2026 09:40
478dfdd to
85dd858
Compare
rursprung
marked this pull request as ready for review
October 26, 2026 09:44
Contributor
Author
|
since #3352 is now merged this is ready for review |
Add `RequestConverter#bulkDeleteOperation()` which creates a delete `BulkOperation` for a document id with an optional routing. The existing bulk conversion only supports `IndexQuery` and `UpdateQuery`, but the upcoming bulk ingester also needs to send delete operations. See spring-projects#2071 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
Add the package `core.bulk` with the types of a bulk ingester that collects index, update and delete operations and sends them in bulk requests when a size, count or time limit is reached: - `BulkIngester` to add operations with an optional context - `BulkIngesterListener` to receive per-item results or a request failure - `BulkIngesterOptions` with the same defaults as the bulk ingester of the Elasticsearch Java client - `BulkOperationResult` and `BulkOperationType` The types don't depend on a specific client so that every client implementation can provide an ingester. See spring-projects#2071 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
Add `ElasticsearchOperations#bulkIngester()` with overloads that take the index from an entity class or use the default options. The default implementation throws an `UnsupportedClientOperationException`, so client implementations that don't provide an ingester keep working without changes. See spring-projects#2071 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
Add `AbstractBulkIngester` which contains the logic of a bulk ingester that does not depend on a client, so that a client implementation only has to convert and send the operations: - argument checks and closed state, `close()` is idempotent - auditing and `BeforeConvertCallback` on the calling thread - id conversion and default routing for deletes - mapping item responses to `BulkOperationResult`s - write-back of id, version, seq_no and primary term and `AfterSaveCallback` for successful index and create operations only - unwrapping and translating request failures - isolating the listener: its exceptions are logged and it can't call the ingester, which could otherwise deadlock The class lives in `core` next to `AbstractElasticsearchTemplate` to use its callback helpers without adding new API to the template. See spring-projects#2071 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
Implement `ElasticsearchTemplate#bulkIngester()` with `ElasticsearchBulkIngester`, which extends `AbstractBulkIngester` and delegates batching, concurrency, backpressure and the flush interval to the `BulkIngester` helper of the Elasticsearch Java client. The request-wide settings from the `BulkOptions` and the refresh policy of the template are applied to every request. Each request is recorded as one `BULK` observation with the actual batch size; a request failure is recorded as observation error. See spring-projects#2071 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
Add a section about the bulk ingester to the reference documentation: usage, integration with callbacks and observations, threading, blocking under backpressure, size overshoot, refresh per request, missing retries and the behaviour of client implementations without support. Mention it in the "What's new" page. See spring-projects#2071 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
rursprung
force-pushed
the
add-bulk-ingester
branch
from
October 29, 2026 06:58
85dd858 to
dcebe0e
Compare
Contributor
Author
|
rebased to resolve merge conflicts now that #3357 has landed. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(generated by copilot/claude, reviewed by me)
Closes #2071
What and why
This adds a
BulkIngestertoElasticsearchOperations. It collects index, update and delete operations one by one and sends them in bulk requests. It follows the direction agreed in #2071: a client-independent API, an implementation inelc, a default that throwsUnsupportedClientOperationExceptionfor other clients, a mandatory listener, imperative only, no retries.bulkIndex/bulkUpdate(also with the size limits from #3357) work on a list that is known in advance and block until it is done. The ingester covers the cases where that does not fit:add()blocks when too many requests are in flight, so memory use stays bounded.Today users can use the native
BulkIngesterdirectly, but then they lose entity mapping, auditing, entity callbacks, write-back of id/version/seq_no/primary_term, exception translation and observations.API overview
New package
org.springframework.data.elasticsearch.core.bulk(no client types):BulkIngester<C>:add(IndexQuery[, C]),add(UpdateQuery[, C]),addDelete(id[, routing, C]),flush(),close(), statistics getters.BulkIngesterListener<C>:afterBulk(executionId, List<BulkOperationResult<C>>)andafterBulk(executionId, List<C> contexts, RuntimeException failure).BulkIngesterOptions:maxOperations,maxSize,maxConcurrentRequests,flushIntervalandBulkOptionsfor the request-wide settings. Defaults are those of the native helper (1000 operations, 5 MiB, 1 concurrent request, no flush interval).BulkOperationResult<C>(record: type, context, id, index, status,IndexedObjectInformation,FailureDetails) andBulkOperationType.Entry point:
ElasticsearchOperations#bulkIngester(BulkIngesterOptions, IndexCoordinates, BulkIngesterListener)plus overloads with an entity class and/or default options.Behaviour
BeforeConvertCallbackrun on the calling thread inadd(). For successful index/create operations the id, version, seq_no and primary term are written back to the entity and theAfterSaveCallbackruns. Failed operations get neither.AfterSaveCallbackrun on threads of the ingester. With more than one concurrent request they can run concurrently. The listener must not calladd/flush/close(that would deadlock in the native helper); the ingester detects this and throwsIllegalStateException. Exceptions thrown by the listener are logged, because the native helper would lose them silently.add()andflush()block whenmaxConcurrentRequestsrequests are in flight. Not for reactive threads.maxSize, so a request can be larger by one operation.BulkOptionsand the refresh policy of the template are applied to every request.CompletionException/ExecutionExceptionand translated with the client's exception translator before it is passed to the listener. Nothing is retried.close()sends the rest, waits for all requests and listener calls, can be called more than once, and does not close the client.addafterclosethrowsIllegalStateException.bulkobservation per request with the real batch size. A request failure is recorded as error. Item failures are not (unlikebulkIndex, where they become aBulkFailureException).Impact on spring-data-opensearch
The change is additive.
OpenSearchTemplategets the defaultbulkIngesterthat throwsUnsupportedClientOperationException, so nothing breaks and nothing has to be done right away.To keep the effort for spring-data-opensearch small, everything that does not depend on the client is in
core.AbstractBulkIngester: argument checks, closed state, before-convert/auditing, id conversion and default routing for deletes, context bookkeeping, mapping of a client-neutralItemResponsetoBulkOperationResult, write-back and after-save callback, unwrapping and translation of request failures, and listener isolation. It lives incorenext toAbstractElasticsearchTemplateso it can use the existing protected callback helpers without new API on the template.OpenSearchTemplateextendsAbstractElasticsearchTemplate, so an adapter can pass it to the base class.opensearch-java3.x has the same helper (org.opensearch.client.opensearch._helpers.bulk.BulkIngester, same builder and listener methods). An OpenSearch adapter only has to:doAdd(IndexQuery, …),doAdd(UpdateQuery, …),doAddDelete(id, routing, …)by converting with itsRequestConverterand calling the nativeadd;doFlush(),doClose()and the seven statistics getters by delegating to the native helper;translateException(Throwable)withOpenSearchExceptionTranslator;BulkListener, map eachBulkResponseItemtoAbstractBulkIngester.ItemResponseand callonResponse(...)/onFailure(...);bulkIngester(...)inOpenSearchTemplate.Its
osc.RequestConverterwould need the equivalents ofbulkRequestBuilder,bulkOperation(#3352) andbulkDeleteOperation. The ELC adapter here is about 200 lines including the observation handling, which OpenSearch does not need.spring-data-opensearch keeps its own copy of
CallbackIntegrationTests, so the new test there does not affect it until it is synced.Compatibility
Additive only: new types, new default methods on
ElasticsearchOperations, a new method inElasticsearchTemplate, a new package-private method inRequestConverter.ElasticsearchTemplate#createObservationchanged from private to package-private. No behaviour change for existing operations.Out of scope / follow-ups
backoffPolicy).addDelete(Object entity)).Open questions
core.AbstractBulkIngester(public but not official API, likeAbstractElasticsearchTemplate) the right place for the shared logic, or would you prefer it incore.bulkwith hooks passed in by the template?bulkobservation as error, likebulkIndexdoes?BulkOptionsalso hasmaxOperations/maxSize, and the template can have defaultBulkOptions. The ingester uses only the limits fromBulkIngesterOptions; it ignores the limits in the nestedBulkOptionsand the template default (documented). Is that acceptable, or should it fall back to them?AfterSaveCallbackon ingester threads are logged and the item is still reported as successful (the document was written). Is that the expected behaviour?Tests
BulkIngesterOptionsUnitTests,ElasticsearchOperationsBulkIngesterUnitTests,AbstractBulkIngesterUnitTests(fake adapter: result mapping, callbacks only on success, listener exception isolation, re-entrance guard, closed state, failure translation),ElasticsearchBulkIngesterUnitTests(mocked transport: request failure translation, request settings, item mapping, listener required),RequestConverterTest(delete conversion).// #2071):ElasticsearchELCIntegrationTests(mixed index/create/update/delete with contexts, generated id and version write-back, partial 409 failure, request splitting, stats, idempotent close, flush interval),CallbackIntegrationTests(BeforeConvert, AfterSave, seq_no/primary_term write-back),ObservabilityIntegrationTests(onebulkobservation per request with batch size).Related
This is based on #3352 and related to #3357