2525import org .elasticsearch .cluster .metadata .InferenceFieldMetadata ;
2626import org .elasticsearch .cluster .metadata .ProjectMetadata ;
2727import org .elasticsearch .cluster .service .ClusterService ;
28+ import org .elasticsearch .common .bytes .BytesArray ;
2829import org .elasticsearch .common .bytes .BytesReference ;
30+ import org .elasticsearch .common .bytes .CompositeBytesReference ;
2931import org .elasticsearch .common .settings .Setting ;
3032import org .elasticsearch .common .unit .ByteSizeValue ;
3133import org .elasticsearch .common .util .concurrent .AtomicArray ;
5254import org .elasticsearch .tasks .Task ;
5355import org .elasticsearch .xcontent .XContent ;
5456import org .elasticsearch .xcontent .XContentBuilder ;
57+ import org .elasticsearch .xcontent .XContentFactory ;
5558import org .elasticsearch .xcontent .XContentParser ;
5659import org .elasticsearch .xcontent .XContentParserConfiguration ;
5760import org .elasticsearch .xcontent .XContentType ;
@@ -469,8 +472,8 @@ private void recordRequestCountMetrics(Model model, int incrementBy, Throwable t
469472 * Adds all inference requests associated with their respective inference IDs to the given {@code requestsMap}
470473 * for the specified {@code item}.
471474 *
472- * @param item The bulk request item to process.
473- * @param itemIndex The position of the item within the original bulk request.
475+ * @param item The bulk request item to process.
476+ * @param itemIndex The position of the item within the original bulk request.
474477 * @param requestsMap A map storing inference requests, where each key is an inference ID,
475478 * and the value is a list of associated {@link FieldInferenceRequest} objects.
476479 * @return The total content length of all newly added requests, or {@code 0} if no requests were added.
@@ -671,27 +674,137 @@ private void applyInferenceResponses(BulkItemRequest item, FieldInferenceRespons
671674 );
672675 inferenceFieldsMap .put (fieldName , result );
673676 }
674-
675- BytesReference originalSource = indexRequest .source ();
676677 if (useLegacyFormat ) {
677678 var newDocMap = indexRequest .sourceAsMap ();
678679 for (var entry : inferenceFieldsMap .entrySet ()) {
679680 SemanticTextUtils .insertValue (entry .getKey (), newDocMap , entry .getValue ());
680681 }
681- indexRequest .source (newDocMap , indexRequest .getContentType ());
682+ XContentBuilder builder = XContentFactory .contentBuilder (indexRequest .getContentType ());
683+ builder .map (newDocMap );
684+ var newSource = BytesReference .bytes (builder );
685+ if (incrementIndexingPressure (item , indexRequest , newSource .length ())) {
686+ indexRequest .source (newSource , indexRequest .getContentType ());
687+ }
688+ } else {
689+ updateSourceWithInferenceFields (item , indexRequest , inferenceFieldsMap );
690+ }
691+ }
692+
693+ /**
694+ * Updates the {@link IndexRequest}'s source to include additional inference fields.
695+ * <p>
696+ * If the original source uses an array-backed {@link BytesReference}, this method attempts an in-place update,
697+ * reusing the existing array where possible and appending additional bytes only if needed.
698+ * <p>
699+ * If the original source is not array-backed, the entire source is replaced with the new source that includes
700+ * the inference fields. In this case, the full size of the new source is accounted for in indexing pressure.
701+ * <p>
702+ * Note: We do not subtract the indexing pressure of the original source since its bytes may be pooled and not
703+ * reclaimable by the garbage collector during the request lifecycle.
704+ *
705+ * @param item The {@link BulkItemRequest} being processed.
706+ * @param indexRequest The {@link IndexRequest} whose source will be updated.
707+ * @param inferenceFieldsMap A map of additional fields to append to the source.
708+ * @throws IOException if building the new source fails.
709+ */
710+ private void updateSourceWithInferenceFields (
711+ BulkItemRequest item ,
712+ IndexRequest indexRequest ,
713+ Map <String , Object > inferenceFieldsMap
714+ ) throws IOException {
715+ var originalSource = indexRequest .source ();
716+ final BytesReference newSource ;
717+
718+ // Build a new source by appending the inference fields to the existing source.
719+ try (XContentBuilder builder = XContentBuilder .builder (indexRequest .getContentType ().xContent ())) {
720+ appendSourceAndInferenceMetadata (builder , originalSource , indexRequest .getContentType (), inferenceFieldsMap );
721+ newSource = BytesReference .bytes (builder );
722+ }
723+
724+ // Calculate the additional size to account for in indexing pressure.
725+ final int additionalSize = originalSource .hasArray () ? newSource .length () - originalSource .length () : newSource .length ();
726+
727+ // If we exceed the indexing pressure limit, do not proceed with the update.
728+ if (incrementIndexingPressure (item , indexRequest , additionalSize ) == false ) {
729+ return ;
730+ }
731+
732+ // Apply the updated source to the index request.
733+ if (originalSource .hasArray ()) {
734+ // If the original source is backed by an array, perform in-place update:
735+ // - Copy as much of the new source as fits into the original array.
736+ System .arraycopy (
737+ newSource .array (),
738+ newSource .arrayOffset (),
739+ originalSource .array (),
740+ originalSource .arrayOffset (),
741+ originalSource .length ()
742+ );
743+
744+ int remainingSize = newSource .length () - originalSource .length ();
745+ if (remainingSize > 0 ) {
746+ // If there are additional bytes, append them as a new BytesArray segment.
747+ byte [] remainingBytes = new byte [remainingSize ];
748+ System .arraycopy (
749+ newSource .array (),
750+ newSource .arrayOffset () + originalSource .length (),
751+ remainingBytes ,
752+ 0 ,
753+ remainingSize
754+ );
755+ indexRequest .source (
756+ CompositeBytesReference .of (originalSource , new BytesArray (remainingBytes )),
757+ indexRequest .getContentType ()
758+ );
759+ } else {
760+ // No additional bytes; just adjust the slice length.
761+ indexRequest .source (originalSource .slice (0 , newSource .length ()));
762+ }
682763 } else {
683- try (XContentBuilder builder = XContentBuilder .builder (indexRequest .getContentType ().xContent ())) {
684- appendSourceAndInferenceMetadata (builder , indexRequest .source (), indexRequest .getContentType (), inferenceFieldsMap );
685- indexRequest .source (builder );
764+ // If the original source is not array-backed, replace it entirely.
765+ indexRequest .source (newSource , indexRequest .getContentType ());
766+ }
767+ }
768+
769+ /**
770+ * Appends the original source and the new inference metadata field directly to the provided
771+ * {@link XContentBuilder}, avoiding the need to materialize the original source as a {@link Map}.
772+ */
773+ private void appendSourceAndInferenceMetadata (
774+ XContentBuilder builder ,
775+ BytesReference source ,
776+ XContentType xContentType ,
777+ Map <String , Object > inferenceFieldsMap
778+ ) throws IOException {
779+ builder .startObject ();
780+
781+ // append the original source
782+ try (
783+ XContentParser parser = XContentHelper .createParserNotCompressed (XContentParserConfiguration .EMPTY , source , xContentType )
784+ ) {
785+ // skip start object
786+ parser .nextToken ();
787+ while (parser .nextToken () != XContentParser .Token .END_OBJECT ) {
788+ builder .copyCurrentStructure (parser );
686789 }
687790 }
688- long modifiedSourceSize = indexRequest .source ().ramBytesUsed ();
689791
690- // Add the indexing pressure from the source modifications.
792+ // add the inference metadata field
793+ builder .field (InferenceMetadataFieldsMapper .NAME );
794+ try (XContentParser parser = XContentHelper .mapToXContentParser (XContentParserConfiguration .EMPTY , inferenceFieldsMap )) {
795+ builder .copyCurrentStructure (parser );
796+ }
797+
798+ builder .endObject ();
799+ }
800+
801+ private boolean incrementIndexingPressure (BulkItemRequest item , IndexRequest indexRequest , int inc ) {
691802 try {
692- coordinatingIndexingPressure .increment (1 , modifiedSourceSize - originalSource .ramBytesUsed ());
803+ if (inc > 0 ) {
804+ coordinatingIndexingPressure .increment (1 , inc );
805+ }
806+ return true ;
693807 } catch (EsRejectedExecutionException e ) {
694- indexRequest .source (originalSource , indexRequest .getContentType ());
695808 inferenceStats .bulkRejection ().incrementBy (1 );
696809 item .abort (
697810 item .index (),
@@ -702,40 +815,11 @@ private void applyInferenceResponses(BulkItemRequest item, FieldInferenceRespons
702815 e
703816 )
704817 );
818+ return false ;
705819 }
706820 }
707821 }
708822
709- /**
710- * Appends the original source and the new inference metadata field directly to the provided
711- * {@link XContentBuilder}, avoiding the need to materialize the original source as a {@link Map}.
712- */
713- private static void appendSourceAndInferenceMetadata (
714- XContentBuilder builder ,
715- BytesReference source ,
716- XContentType xContentType ,
717- Map <String , Object > inferenceFieldsMap
718- ) throws IOException {
719- builder .startObject ();
720-
721- // append the original source
722- try (XContentParser parser = XContentHelper .createParserNotCompressed (XContentParserConfiguration .EMPTY , source , xContentType )) {
723- // skip start object
724- parser .nextToken ();
725- while (parser .nextToken () != XContentParser .Token .END_OBJECT ) {
726- builder .copyCurrentStructure (parser );
727- }
728- }
729-
730- // add the inference metadata field
731- builder .field (InferenceMetadataFieldsMapper .NAME );
732- try (XContentParser parser = XContentHelper .mapToXContentParser (XContentParserConfiguration .EMPTY , inferenceFieldsMap )) {
733- builder .copyCurrentStructure (parser );
734- }
735-
736- builder .endObject ();
737- }
738-
739823 static IndexRequest getIndexRequestOrNull (DocWriteRequest <?> docWriteRequest ) {
740824 if (docWriteRequest instanceof IndexRequest indexRequest ) {
741825 return indexRequest ;
0 commit comments