@@ -149,6 +149,7 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie
149149 public static final NodeFeature SEMANTIC_TEXT_SPARSE_VECTOR_INDEX_OPTIONS = new NodeFeature (
150150 "semantic_text.sparse_vector_index_options"
151151 );
152+ public static final NodeFeature SEMANTIC_TEXT_UPDATABLE_INFERENCE_ID = new NodeFeature ("semantic_text.updatable_inference_id" );
152153
153154 public static final String CONTENT_TYPE = "semantic_text" ;
154155 public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID ;
@@ -242,7 +243,7 @@ public Builder(
242243
243244 this .inferenceId = Parameter .stringParam (
244245 INFERENCE_ID_FIELD ,
245- false ,
246+ true ,
246247 mapper -> ((SemanticTextFieldType ) mapper .fieldType ()).inferenceId ,
247248 DEFAULT_ELSER_2_INFERENCE_ID
248249 ).addValidator (v -> {
@@ -325,9 +326,68 @@ protected Parameter<?>[] getParameters() {
325326 @ Override
326327 protected void merge (FieldMapper mergeWith , Conflicts conflicts , MapperMergeContext mapperMergeContext ) {
327328 SemanticTextFieldMapper semanticMergeWith = (SemanticTextFieldMapper ) mergeWith ;
328- semanticMergeWith = copySettings (semanticMergeWith , mapperMergeContext );
329329
330- // We make sure to merge the inference field first to catch any model conflicts
330+ final boolean isInferenceIdUpdate = semanticMergeWith .fieldType ().inferenceId .equals (inferenceId .get ()) == false ;
331+ final boolean hasExplicitModelSettings = modelSettings .get () != null ;
332+
333+ if (isInferenceIdUpdate && hasExplicitModelSettings ) {
334+ validateModelsAreCompatibleWhenInferenceIdIsUpdated (semanticMergeWith .fieldType ().inferenceId , conflicts );
335+ // As the mapper previously had explicit model settings, we need to apply to the new merged mapper
336+ // the resolved model settings if not explicitly set.
337+ semanticMergeWith = copyWithNewModelSettingsIfNotSet (
338+ semanticMergeWith ,
339+ modelRegistry .getMinimalServiceSettings (semanticMergeWith .fieldType ().inferenceId ),
340+ mapperMergeContext
341+ );
342+ }
343+
344+ semanticMergeWith = copyModelSettingsIfNotSet (semanticMergeWith , mapperMergeContext );
345+
346+ // We make sure to merge the inference field first to catch any model conflicts.
347+ // If inference_id is updated and there are no explicit model settings, we should be
348+ // able to switch to the new inference field without the need to check for conflicts.
349+ if (isInferenceIdUpdate == false || hasExplicitModelSettings ) {
350+ mergeInferenceField (mapperMergeContext , semanticMergeWith );
351+ }
352+
353+ super .merge (semanticMergeWith , conflicts , mapperMergeContext );
354+ conflicts .check ();
355+ }
356+
357+ private void validateModelsAreCompatibleWhenInferenceIdIsUpdated (String newInferenceId , Conflicts conflicts ) {
358+ MinimalServiceSettings currentModelSettings = modelSettings .get ();
359+ MinimalServiceSettings updatedModelSettings = modelRegistry .getMinimalServiceSettings (newInferenceId );
360+ if (currentModelSettings != null && updatedModelSettings == null ) {
361+ throw new IllegalArgumentException (
362+ "Cannot merge ["
363+ + CONTENT_TYPE
364+ + "] field ["
365+ + leafName ()
366+ + "] because inference endpoint ["
367+ + newInferenceId
368+ + "] does not exist."
369+ );
370+ }
371+ if (canMergeModelSettings (currentModelSettings , updatedModelSettings , conflicts ) == false ) {
372+ throw new IllegalArgumentException (
373+ "Cannot merge ["
374+ + CONTENT_TYPE
375+ + "] field ["
376+ + leafName ()
377+ + "] because inference endpoint ["
378+ + inferenceId .get ()
379+ + "] with model settings ["
380+ + currentModelSettings
381+ + "] is not compatible with new inference endpoint ["
382+ + newInferenceId
383+ + "] with model settings ["
384+ + updatedModelSettings
385+ + "]."
386+ );
387+ }
388+ }
389+
390+ private void mergeInferenceField (MapperMergeContext mapperMergeContext , SemanticTextFieldMapper semanticMergeWith ) {
331391 try {
332392 var context = mapperMergeContext .createChildContext (semanticMergeWith .leafName (), ObjectMapper .Dynamic .FALSE );
333393 var inferenceField = inferenceFieldBuilder .apply (context .getMapperBuilderContext ());
@@ -340,9 +400,6 @@ protected void merge(FieldMapper mergeWith, Conflicts conflicts, MapperMergeCont
340400 : "" ;
341401 throw new IllegalArgumentException (errorMessage , e );
342402 }
343-
344- super .merge (semanticMergeWith , conflicts , mapperMergeContext );
345- conflicts .check ();
346403 }
347404
348405 /**
@@ -498,18 +555,35 @@ private void validateIndexOptions(SemanticTextIndexOptions indexOptions, String
498555 }
499556
500557 /**
501- * As necessary, copy settings from this builder to the passed-in mapper.
558+ * As necessary, copy model settings from this builder to the passed-in mapper.
502559 * Used to preserve {@link MinimalServiceSettings} when updating a semantic text mapping to one where the model settings
503560 * are not specified.
504561 *
505562 * @param mapper The mapper
506563 * @return A mapper with the copied settings applied
507564 */
508- private SemanticTextFieldMapper copySettings (SemanticTextFieldMapper mapper , MapperMergeContext mapperMergeContext ) {
565+ private SemanticTextFieldMapper copyModelSettingsIfNotSet (SemanticTextFieldMapper mapper , MapperMergeContext mapperMergeContext ) {
566+ return copyWithNewModelSettingsIfNotSet (mapper , modelSettings .getValue (), mapperMergeContext );
567+ }
568+
569+ /**
570+ * Creates a new mapper with the new model settings if model settings are not set on the mapper.
571+ * If the mapper already has model settings or the new model settings are null, the mapper is
572+ * returned unchanged.
573+ *
574+ * @param mapper The mapper
575+ * @param modelSettings the new model settings. If null the mapper will be returned unchanged.
576+ * @return A mapper with the copied settings applied
577+ */
578+ private SemanticTextFieldMapper copyWithNewModelSettingsIfNotSet (
579+ SemanticTextFieldMapper mapper ,
580+ @ Nullable MinimalServiceSettings modelSettings ,
581+ MapperMergeContext mapperMergeContext
582+ ) {
509583 SemanticTextFieldMapper returnedMapper = mapper ;
510584 if (mapper .fieldType ().getModelSettings () == null ) {
511585 Builder builder = from (mapper );
512- builder .setModelSettings (modelSettings . getValue () );
586+ builder .setModelSettings (modelSettings );
513587 returnedMapper = builder .build (mapperMergeContext .getMapperBuilderContext ());
514588 }
515589
@@ -783,6 +857,11 @@ protected void doValidate(MappingLookup mappers) {
783857 }
784858 }
785859
860+ @ Override
861+ protected void checkIncomingMergeType (FieldMapper mergeWith ) {
862+ super .checkIncomingMergeType (mergeWith );
863+ }
864+
786865 public static class SemanticTextFieldType extends SimpleMappedFieldType {
787866 private final String inferenceId ;
788867 private final String searchInferenceId ;
0 commit comments