diff --git a/docs/changelog/136120.yaml b/docs/changelog/136120.yaml new file mode 100644 index 0000000000000..861784fd25c58 --- /dev/null +++ b/docs/changelog/136120.yaml @@ -0,0 +1,5 @@ +pr: 136120 +summary: Allow updating `inference_id` of `semantic_text` fields +area: "Mapping" +type: enhancement +issues: [] diff --git a/docs/reference/elasticsearch/mapping-reference/semantic-text.md b/docs/reference/elasticsearch/mapping-reference/semantic-text.md index 5ef7bca014d60..8978f5f65d950 100644 --- a/docs/reference/elasticsearch/mapping-reference/semantic-text.md +++ b/docs/reference/elasticsearch/mapping-reference/semantic-text.md @@ -138,12 +138,31 @@ While we do encourage experimentation, we do not recommend implementing producti `inference_id` : (Optional, string) {{infer-cap}} endpoint that will be used to generate -embeddings for the field. By default, `.elser-2-elasticsearch` is used. This -parameter cannot be updated. Use -the [Create {{infer}} API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put) +embeddings for the field. By default, `.elser-2-elasticsearch` is used. +Use the [Create {{infer}} API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put) to create the endpoint. If `search_inference_id` is specified, the {{infer}} endpoint will only be used at index time. +::::{applies-switch} + +:::{applies-item} { "stack": "ga 9.0" } +This parameter cannot be updated. +::: + +:::{applies-item} { "stack": "ga 9.3" } + +You can update this parameter by using +the [Update mapping API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-put-mapping). +You can update the inference endpoint if no values have been indexed or if the new endpoint is compatible with the current one. + +::::{warning} +When updating an `inference_id` it is important to ensure the new {{infer}} endpoint produces the correct embeddings for your use case. This typically means using the same underlying model. +:::: + +::: + +:::: + `search_inference_id` : (Optional, string) {{infer-cap}} endpoint that will be used to generate embeddings at query time. You can update this parameter by using diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceGetServicesIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceGetServicesIT.java index f86c92c02db48..dd1012daffc03 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceGetServicesIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceGetServicesIT.java @@ -74,6 +74,7 @@ public void testGetServicesWithoutTaskType() throws IOException { "completion_test_service", "test_reranking_service", "test_service", + "alternate_sparse_embedding_test_service", "text_embedding_test_service", "voyageai", "watsonxai", @@ -209,6 +210,7 @@ public void testGetServicesWithSparseEmbeddingTaskType() throws IOException { "hugging_face", "streaming_completion_test_service", "test_service", + "alternate_sparse_embedding_test_service", "amazon_sagemaker" ).toArray() ) diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java index 86dcb56fa369d..2202b7cf53a08 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.util.LazyInitializable; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.inference.ChunkInferenceInput; @@ -43,12 +42,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; public class TestSparseInferenceServiceExtension implements InferenceServiceExtension { @Override public List getInferenceServiceFactories() { - return List.of(TestInferenceService::new); + return List.of(TestInferenceService::new, TestAlternateSparseInferenceService::new); } public static class TestSparseModel extends Model { @@ -60,16 +60,40 @@ public TestSparseModel(String inferenceEntityId, TestServiceSettings serviceSett } } - public static class TestInferenceService extends AbstractTestInferenceService { + public static class TestInferenceService extends AbstractSparseTestInferenceService { public static final String NAME = "test_service"; + public TestInferenceService(InferenceServiceFactoryContext inferenceServiceFactoryContext) {} + + @Override + protected String testServiceName() { + return NAME; + } + } + + /** + * A second sparse service allows testing updates from one service to another. + */ + public static class TestAlternateSparseInferenceService extends AbstractSparseTestInferenceService { + public static final String NAME = "alternate_sparse_embedding_test_service"; + + public TestAlternateSparseInferenceService(InferenceServiceFactoryContext inferenceServiceFactoryContext) {} + + @Override + protected String testServiceName() { + return NAME; + } + } + + abstract static class AbstractSparseTestInferenceService extends AbstractTestInferenceService { + private static final EnumSet supportedTaskTypes = EnumSet.of(TaskType.SPARSE_EMBEDDING); - public TestInferenceService(InferenceServiceExtension.InferenceServiceFactoryContext context) {} + protected abstract String testServiceName(); @Override public String name() { - return NAME; + return testServiceName(); } @Override @@ -92,7 +116,7 @@ public void parseRequestConfig( @Override public InferenceServiceConfiguration getConfiguration() { - return Configuration.get(); + return new Configuration(testServiceName()).get(); } @Override @@ -195,41 +219,43 @@ private static float generateEmbedding(String input, int position) { } public static class Configuration { - public static InferenceServiceConfiguration get() { - return configuration.getOrCompute(); + + private final String serviceName; + + Configuration(String serviceName) { + this.serviceName = Objects.requireNonNull(serviceName); + } + + InferenceServiceConfiguration get() { + var configurationMap = new HashMap(); + + configurationMap.put( + "model", + new SettingsConfiguration.Builder(EnumSet.of(TaskType.SPARSE_EMBEDDING)).setDescription("") + .setLabel("Model") + .setRequired(true) + .setSensitive(false) + .setType(SettingsConfigurationFieldType.STRING) + .build() + ); + + configurationMap.put( + "hidden_field", + new SettingsConfiguration.Builder(EnumSet.of(TaskType.SPARSE_EMBEDDING)).setDescription("") + .setLabel("Hidden Field") + .setRequired(true) + .setSensitive(false) + .setType(SettingsConfigurationFieldType.STRING) + .build() + ); + + return new InferenceServiceConfiguration.Builder().setService(serviceName) + .setName(serviceName) + .setTaskTypes(supportedTaskTypes) + .setConfigurations(configurationMap) + .build(); } - private static final LazyInitializable configuration = new LazyInitializable<>( - () -> { - var configurationMap = new HashMap(); - - configurationMap.put( - "model", - new SettingsConfiguration.Builder(EnumSet.of(TaskType.SPARSE_EMBEDDING)).setDescription("") - .setLabel("Model") - .setRequired(true) - .setSensitive(false) - .setType(SettingsConfigurationFieldType.STRING) - .build() - ); - - configurationMap.put( - "hidden_field", - new SettingsConfiguration.Builder(EnumSet.of(TaskType.SPARSE_EMBEDDING)).setDescription("") - .setLabel("Hidden Field") - .setRequired(true) - .setSensitive(false) - .setType(SettingsConfigurationFieldType.STRING) - .build() - ); - - return new InferenceServiceConfiguration.Builder().setService(NAME) - .setName(NAME) - .setTaskTypes(supportedTaskTypes) - .setConfigurations(configurationMap) - .build(); - } - ); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java index e15759669785f..bae88e6372fdb 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java @@ -24,6 +24,7 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_INDEX_OPTIONS_WITH_DEFAULTS; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_SPARSE_VECTOR_INDEX_OPTIONS; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_SUPPORT_CHUNKING_CONFIG; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_UPDATABLE_INFERENCE_ID; import static org.elasticsearch.xpack.inference.queries.LegacySemanticKnnVectorQueryRewriteInterceptor.SEMANTIC_KNN_FILTER_FIX; import static org.elasticsearch.xpack.inference.queries.LegacySemanticKnnVectorQueryRewriteInterceptor.SEMANTIC_KNN_VECTOR_QUERY_REWRITE_INTERCEPTION_SUPPORTED; import static org.elasticsearch.xpack.inference.queries.LegacySemanticMatchQueryRewriteInterceptor.SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED; @@ -93,6 +94,7 @@ public Set getTestFeatures() { SEMANTIC_TEXT_HIGHLIGHTING_FLAT, SEMANTIC_TEXT_SPARSE_VECTOR_INDEX_OPTIONS, SEMANTIC_TEXT_FIELDS_CHUNKS_FORMAT, + SEMANTIC_TEXT_UPDATABLE_INFERENCE_ID, SemanticQueryBuilder.SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS, SemanticQueryBuilder.SEMANTIC_QUERY_FILTER_FIELD_CAPS_FIX, InterceptedInferenceQueryBuilder.NEW_SEMANTIC_QUERY_INTERCEPTORS, diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 1a8b162eb1b46..3ecb884c47f49 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -150,6 +150,7 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie public static final NodeFeature SEMANTIC_TEXT_SPARSE_VECTOR_INDEX_OPTIONS = new NodeFeature( "semantic_text.sparse_vector_index_options" ); + public static final NodeFeature SEMANTIC_TEXT_UPDATABLE_INFERENCE_ID = new NodeFeature("semantic_text.updatable_inference_id"); public static final String CONTENT_TYPE = "semantic_text"; public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID; @@ -243,7 +244,7 @@ public Builder( this.inferenceId = Parameter.stringParam( INFERENCE_ID_FIELD, - false, + true, mapper -> ((SemanticTextFieldType) mapper.fieldType()).inferenceId, DEFAULT_ELSER_2_INFERENCE_ID ).addValidator(v -> { @@ -326,9 +327,65 @@ protected Parameter[] getParameters() { @Override protected void merge(FieldMapper mergeWith, Conflicts conflicts, MapperMergeContext mapperMergeContext) { SemanticTextFieldMapper semanticMergeWith = (SemanticTextFieldMapper) mergeWith; - semanticMergeWith = copySettings(semanticMergeWith, mapperMergeContext); - // We make sure to merge the inference field first to catch any model conflicts + final boolean isInferenceIdUpdate = semanticMergeWith.fieldType().inferenceId.equals(inferenceId.get()) == false; + final boolean hasExplicitModelSettings = modelSettings.get() != null; + + MinimalServiceSettings updatedModelSettings = modelSettings.get(); + if (isInferenceIdUpdate && hasExplicitModelSettings) { + validateModelsAreCompatibleWhenInferenceIdIsUpdated(semanticMergeWith.fieldType().inferenceId, conflicts); + // As the mapper previously had explicit model settings, we need to apply to the new merged mapper + // the resolved model settings if not explicitly set. + updatedModelSettings = modelRegistry.getMinimalServiceSettings(semanticMergeWith.fieldType().inferenceId); + } + + semanticMergeWith = copyWithNewModelSettingsIfNotSet(semanticMergeWith, updatedModelSettings, mapperMergeContext); + + // We make sure to merge the inference field first to catch any model conflicts. + // If inference_id is updated and there are no explicit model settings, we should be + // able to switch to the new inference field without the need to check for conflicts. + if (isInferenceIdUpdate == false || hasExplicitModelSettings) { + mergeInferenceField(mapperMergeContext, semanticMergeWith); + } + + super.merge(semanticMergeWith, conflicts, mapperMergeContext); + conflicts.check(); + } + + private void validateModelsAreCompatibleWhenInferenceIdIsUpdated(String newInferenceId, Conflicts conflicts) { + MinimalServiceSettings currentModelSettings = modelSettings.get(); + MinimalServiceSettings updatedModelSettings = modelRegistry.getMinimalServiceSettings(newInferenceId); + if (currentModelSettings != null && updatedModelSettings == null) { + throw new IllegalArgumentException( + "Cannot update [" + + CONTENT_TYPE + + "] field [" + + leafName() + + "] because inference endpoint [" + + newInferenceId + + "] does not exist." + ); + } + if (canMergeModelSettings(currentModelSettings, updatedModelSettings, conflicts) == false) { + throw new IllegalArgumentException( + "Cannot update [" + + CONTENT_TYPE + + "] field [" + + leafName() + + "] because inference endpoint [" + + inferenceId.get() + + "] with model settings [" + + currentModelSettings + + "] is not compatible with new inference endpoint [" + + newInferenceId + + "] with model settings [" + + updatedModelSettings + + "]." + ); + } + } + + private void mergeInferenceField(MapperMergeContext mapperMergeContext, SemanticTextFieldMapper semanticMergeWith) { try { var context = mapperMergeContext.createChildContext(semanticMergeWith.leafName(), ObjectMapper.Dynamic.FALSE); var inferenceField = inferenceFieldBuilder.apply(context.getMapperBuilderContext()); @@ -341,9 +398,6 @@ protected void merge(FieldMapper mergeWith, Conflicts conflicts, MapperMergeCont : ""; throw new IllegalArgumentException(errorMessage, e); } - - super.merge(semanticMergeWith, conflicts, mapperMergeContext); - conflicts.check(); } /** @@ -499,18 +553,23 @@ private void validateIndexOptions(SemanticTextIndexOptions indexOptions, String } /** - * As necessary, copy settings from this builder to the passed-in mapper. - * Used to preserve {@link MinimalServiceSettings} when updating a semantic text mapping to one where the model settings - * are not specified. + * Creates a new mapper with the new model settings if model settings are not set on the mapper. + * If the mapper already has model settings or the new model settings are null, the mapper is + * returned unchanged. * - * @param mapper The mapper + * @param mapper The mapper + * @param modelSettings the new model settings. If null the mapper will be returned unchanged. * @return A mapper with the copied settings applied */ - private SemanticTextFieldMapper copySettings(SemanticTextFieldMapper mapper, MapperMergeContext mapperMergeContext) { + private SemanticTextFieldMapper copyWithNewModelSettingsIfNotSet( + SemanticTextFieldMapper mapper, + @Nullable MinimalServiceSettings modelSettings, + MapperMergeContext mapperMergeContext + ) { SemanticTextFieldMapper returnedMapper = mapper; if (mapper.fieldType().getModelSettings() == null) { Builder builder = from(mapper); - builder.setModelSettings(modelSettings.getValue()); + builder.setModelSettings(modelSettings); returnedMapper = builder.build(mapperMergeContext.getMapperBuilderContext()); } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java index f47d7c4c37261..ca93262289fef 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java @@ -119,6 +119,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -494,21 +495,401 @@ public void testMultiFieldsSupport() throws IOException { } } - public void testUpdatesToInferenceIdNotSupported() throws IOException { + public void testUpdateInferenceId_GivenPreviousAndNewDoNotExist() throws IOException { String fieldName = randomAlphaOfLengthBetween(5, 15); MapperService mapperService = createMapperService( mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", "test_model").endObject()), useLegacyFormat ); assertSemanticTextField(mapperService, fieldName, false, null, null); - Exception e = expectThrows( + + merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", "another_model").endObject()) + ); + + assertInferenceEndpoints(mapperService, fieldName, "another_model", "another_model"); + assertSemanticTextField(mapperService, fieldName, false, null, null); + } + + public void testUpdateInferenceId_GivenCurrentHasNoSetModelSettingsAndNewDoesNotExist() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.sparseEmbedding("previous_service"); + givenModelSettings(oldInferenceId, previousModelSettings); + var mapperService = createMapperService( + mapping( + b -> b.startObject(fieldName) + .field("type", SemanticTextFieldMapper.CONTENT_TYPE) + .field(INFERENCE_ID_FIELD, oldInferenceId) + .endObject() + ), + useLegacyFormat + ); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, false, null, null); + + String newInferenceId = "new_inference_id"; + merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ); + + assertInferenceEndpoints(mapperService, fieldName, newInferenceId, newInferenceId); + assertSemanticTextField(mapperService, fieldName, false, null, null); + } + + public void testUpdateInferenceId_GivenCurrentHasModelSettingsAndNewDoesNotExist() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.sparseEmbedding("previous_service"); + givenModelSettings(oldInferenceId, previousModelSettings); + var mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + String newInferenceId = "new_inference_id"; + + Exception exc = expectThrows( IllegalArgumentException.class, () -> merge( mapperService, - mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", "another_model").endObject()) + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ) + ); + + assertThat( + exc.getMessage(), + containsString( + "Cannot update [semantic_text] field [" + + fieldName + + "] because inference endpoint [" + + newInferenceId + + "] does not exist." + ) + ); + } + + public void testUpdateInferenceId_GivenCurrentHasNoModelSettingsAndNewIsIncompatibleTaskType_ShouldSucceed() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.sparseEmbedding("previous_service"); + givenModelSettings(oldInferenceId, previousModelSettings); + var mapperService = createMapperService( + mapping( + b -> b.startObject(fieldName) + .field("type", SemanticTextFieldMapper.CONTENT_TYPE) + .field(INFERENCE_ID_FIELD, oldInferenceId) + .endObject() + ), + useLegacyFormat + ); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, false, null, null); + + String newInferenceId = "new_inference_id"; + MinimalServiceSettings newModelSettings = MinimalServiceSettings.textEmbedding( + "new_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BIT + ); + givenModelSettings(newInferenceId, newModelSettings); + merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ); + + assertInferenceEndpoints(mapperService, fieldName, newInferenceId, newInferenceId); + assertSemanticTextField(mapperService, fieldName, false, null, null); + } + + public void testUpdateInferenceId_GivenCurrentHasSparseModelSettingsAndNewIsCompatible() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.sparseEmbedding("previous_service"); + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + String newInferenceId = "new_inference_id"; + MinimalServiceSettings newModelSettings = MinimalServiceSettings.sparseEmbedding("new_service"); + givenModelSettings(newInferenceId, newModelSettings); + merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ); + + assertInferenceEndpoints(mapperService, fieldName, newInferenceId, newInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + SemanticTextFieldMapper semanticFieldMapper = getSemanticFieldMapper(mapperService, fieldName); + assertThat(semanticFieldMapper.fieldType().getModelSettings(), equalTo(newModelSettings)); + } + + public void testUpdateInferenceId_GivenCurrentHasSparseModelSettingsAndNewSetsDefault() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.sparseEmbedding("previous_service"); + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + MinimalServiceSettings newModelSettings = MinimalServiceSettings.sparseEmbedding("new_service"); + givenModelSettings(DEFAULT_ELSER_2_INFERENCE_ID, newModelSettings); + merge(mapperService, mapping(b -> b.startObject(fieldName).field("type", "semantic_text").endObject())); + + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); + assertSemanticTextField(mapperService, fieldName, true, null, null); + SemanticTextFieldMapper semanticFieldMapper = getSemanticFieldMapper(mapperService, fieldName); + assertThat(semanticFieldMapper.fieldType().getModelSettings(), equalTo(newModelSettings)); + } + + public void testUpdateInferenceId_GivenCurrentHasSparseModelSettingsAndNewIsIncompatibleTaskType() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.sparseEmbedding("previous_service"); + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + String newInferenceId = "new_inference_id"; + MinimalServiceSettings newModelSettings = MinimalServiceSettings.textEmbedding( + "new_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BIT + ); + givenModelSettings(newInferenceId, newModelSettings); + + Exception exc = expectThrows( + IllegalArgumentException.class, + () -> merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ) + ); + + assertThat( + exc.getMessage(), + containsString( + "Cannot update [semantic_text] field [" + + fieldName + + "] because inference endpoint [" + + oldInferenceId + + "] with model settings [" + + previousModelSettings + + "] is not compatible with new inference endpoint [" + + newInferenceId + + "] with model settings [" + + newModelSettings + + "]" + ) + ); + } + + public void testUpdateInferenceId_GivenCurrentHasDenseModelSettingsAndNewIsCompatible() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.textEmbedding( + "previous_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BIT + ); + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + String newInferenceId = "new_inference_id"; + MinimalServiceSettings newModelSettings = MinimalServiceSettings.textEmbedding( + "new_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BIT + ); + givenModelSettings(newInferenceId, newModelSettings); + merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ); + + assertInferenceEndpoints(mapperService, fieldName, newInferenceId, newInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + SemanticTextFieldMapper semanticFieldMapper = getSemanticFieldMapper(mapperService, fieldName); + assertThat(semanticFieldMapper.fieldType().getModelSettings(), equalTo(newModelSettings)); + } + + public void testUpdateInferenceId_GivenCurrentHasDenseModelSettingsAndNewIsIncompatibleTaskType() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.textEmbedding( + "previous_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BIT + ); + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + String newInferenceId = "new_inference_id"; + MinimalServiceSettings newModelSettings = MinimalServiceSettings.sparseEmbedding("new_service"); + givenModelSettings(newInferenceId, newModelSettings); + + Exception exc = expectThrows( + IllegalArgumentException.class, + () -> merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ) + ); + + assertThat( + exc.getMessage(), + containsString( + "Cannot update [semantic_text] field [" + + fieldName + + "] because inference endpoint [" + + oldInferenceId + + "] with model settings [" + + previousModelSettings + + "] is not compatible with new inference endpoint [" + + newInferenceId + + "] with model settings [" + + newModelSettings + + "]" + ) + ); + } + + public void testUpdateInferenceId_GivenCurrentHasDenseModelSettingsAndNewHasIncompatibleDimensions() throws IOException { + testUpdateInferenceId_GivenDenseModelsWithDifferentSettings( + MinimalServiceSettings.textEmbedding("previous_service", 48, SimilarityMeasure.L2_NORM, DenseVectorFieldMapper.ElementType.BIT), + MinimalServiceSettings.textEmbedding("new_service", 40, SimilarityMeasure.L2_NORM, DenseVectorFieldMapper.ElementType.BIT) + ); + } + + public void testUpdateInferenceId_GivenCurrentHasDenseModelSettingsAndNewHasIncompatibleSimilarityMeasure() throws IOException { + testUpdateInferenceId_GivenDenseModelsWithDifferentSettings( + MinimalServiceSettings.textEmbedding( + "previous_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BYTE + ), + MinimalServiceSettings.textEmbedding("new_service", 48, SimilarityMeasure.COSINE, DenseVectorFieldMapper.ElementType.BYTE) + ); + } + + public void testUpdateInferenceId_GivenCurrentHasDenseModelSettingsAndNewHasIncompatibleElementType() throws IOException { + testUpdateInferenceId_GivenDenseModelsWithDifferentSettings( + MinimalServiceSettings.textEmbedding( + "previous_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BYTE + ), + MinimalServiceSettings.textEmbedding("new_service", 48, SimilarityMeasure.L2_NORM, DenseVectorFieldMapper.ElementType.BIT) + ); + } + + public void testUpdateInferenceId_CurrentHasDenseModelSettingsAndNewSetsDefault_ShouldFailAsDefaultIsSparse() throws IOException { + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + MinimalServiceSettings previousModelSettings = MinimalServiceSettings.textEmbedding( + "previous_service", + 48, + SimilarityMeasure.L2_NORM, + DenseVectorFieldMapper.ElementType.BIT + ); + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + MinimalServiceSettings newModelSettings = MinimalServiceSettings.sparseEmbedding("new_service"); + givenModelSettings(DEFAULT_ELSER_2_INFERENCE_ID, newModelSettings); + + Exception exc = expectThrows( + IllegalArgumentException.class, + () -> merge(mapperService, mapping(b -> b.startObject(fieldName).field("type", "semantic_text").endObject())) + ); + + assertThat( + exc.getMessage(), + containsString( + "Cannot update [semantic_text] field [" + + fieldName + + "] because inference endpoint [" + + oldInferenceId + + "] with model settings [" + + previousModelSettings + + "] is not compatible with new inference endpoint [" + + DEFAULT_ELSER_2_INFERENCE_ID + + "] with model settings [" + + newModelSettings + + "]" + ) + ); + } + + private void testUpdateInferenceId_GivenDenseModelsWithDifferentSettings( + MinimalServiceSettings previousModelSettings, + MinimalServiceSettings newModelSettings + ) throws IOException { + assertThat(previousModelSettings.taskType(), equalTo(TaskType.TEXT_EMBEDDING)); + assertThat(newModelSettings.taskType(), equalTo(TaskType.TEXT_EMBEDDING)); + assertThat(newModelSettings, not(equalTo(previousModelSettings))); + + String fieldName = randomAlphaOfLengthBetween(5, 15); + String oldInferenceId = "old_inference_id"; + givenModelSettings(oldInferenceId, previousModelSettings); + MapperService mapperService = mapperServiceForFieldWithModelSettings(fieldName, oldInferenceId, previousModelSettings); + + assertInferenceEndpoints(mapperService, fieldName, oldInferenceId, oldInferenceId); + assertSemanticTextField(mapperService, fieldName, true, null, null); + + String newInferenceId = "new_inference_id"; + givenModelSettings(newInferenceId, newModelSettings); + + Exception exc = expectThrows( + IllegalArgumentException.class, + () -> merge( + mapperService, + mapping(b -> b.startObject(fieldName).field("type", "semantic_text").field("inference_id", newInferenceId).endObject()) + ) + ); + + assertThat( + exc.getMessage(), + containsString( + "Cannot update [semantic_text] field [" + + fieldName + + "] because inference endpoint [" + + oldInferenceId + + "] with model settings [" + + previousModelSettings + + "] is not compatible with new inference endpoint [" + + newInferenceId + + "] with model settings [" + + newModelSettings + + "]" ) ); - assertThat(e.getMessage(), containsString("Cannot update parameter [inference_id] from [test_model] to [another_model]")); } public void testDynamicUpdate() throws IOException { @@ -778,10 +1159,7 @@ private static void assertSemanticTextField( ChunkingSettings expectedChunkingSettings, SemanticTextIndexOptions expectedIndexOptions ) { - Mapper mapper = mapperService.mappingLookup().getMapper(fieldName); - assertNotNull(mapper); - assertThat(mapper, instanceOf(SemanticTextFieldMapper.class)); - SemanticTextFieldMapper semanticFieldMapper = (SemanticTextFieldMapper) mapper; + SemanticTextFieldMapper semanticFieldMapper = getSemanticFieldMapper(mapperService, fieldName); var fieldType = mapperService.fieldType(fieldName); assertNotNull(fieldType); @@ -857,6 +1235,13 @@ private static void assertSemanticTextField( } } + private static SemanticTextFieldMapper getSemanticFieldMapper(MapperService mapperService, String fieldName) { + Mapper mapper = mapperService.mappingLookup().getMapper(fieldName); + assertNotNull(mapper); + assertThat(mapper, instanceOf(SemanticTextFieldMapper.class)); + return (SemanticTextFieldMapper) mapper; + } + private static void assertInferenceEndpoints( MapperService mapperService, String fieldName, @@ -1111,9 +1496,7 @@ public void testDenseVectorElementType() throws IOException { final String inferenceId = "test_service"; BiConsumer assertMapperService = (m, e) -> { - Mapper mapper = m.mappingLookup().getMapper(fieldName); - assertThat(mapper, instanceOf(SemanticTextFieldMapper.class)); - SemanticTextFieldMapper semanticTextFieldMapper = (SemanticTextFieldMapper) mapper; + SemanticTextFieldMapper semanticTextFieldMapper = getSemanticFieldMapper(m, fieldName); assertThat(semanticTextFieldMapper.fieldType().getModelSettings().elementType(), equalTo(e)); }; @@ -1914,4 +2297,8 @@ private static void assertSparseFeatures(LuceneDocument doc, String fieldName, i } assertThat(count, equalTo(expectedCount)); } + + private void givenModelSettings(String inferenceId, MinimalServiceSettings modelSettings) { + when(globalModelRegistry.getMinimalServiceSettings(inferenceId)).thenReturn(modelSettings); + } } diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml index 88f20d0c5fa6d..9a150aa34a29e 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml @@ -1479,3 +1479,353 @@ setup: - match: { "test-index-options-sparse2.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.prune": true } - match: { "test-index-options-sparse2.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 5.0 } - match: { "test-index-options-sparse2.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.4 } + +--- +"Updating inference_id to different task type should succeed given no model settings": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id } + - not_exists: test-index.mappings.properties.sparse_field.model_settings + + - do: + indices.put_mapping: + index: test-index + body: + properties: + sparse_field: + type: semantic_text + inference_id: dense-inference-id + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": dense-inference-id } + - not_exists: test-index.mappings.properties.sparse_field.model_settings + +--- +"Updating inference_id to non existing endpoint should succeed given no model settings": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id } + - not_exists: test-index.mappings.properties.sparse_field.model_settings + + - do: + indices.put_mapping: + index: test-index + body: + properties: + sparse_field: + type: semantic_text + inference_id: non-existing-inference-id + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": non-existing-inference-id } + - not_exists: test-index.mappings.properties.sparse_field.model_settings + +--- +"Updating inference_id to non existing endpoint should fail given model settings": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id } + - match: { "test-index.mappings.properties.sparse_field.model_settings.service": test_service } + + - do: + catch: /non-existing-inference-id does not exist in this cluster./ + indices.put_mapping: + index: test-index + body: + properties: + sparse_field: + type: semantic_text + inference_id: non-existing-inference-id + +--- +"Updating inference_id to different task type should fail given model settings": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id } + - match: { "test-index.mappings.properties.sparse_field.model_settings.service": test_service } + + - do: + catch: /Cannot update \[semantic_text\] field \[sparse_field\].*because inference endpoint \[sparse-inference-id\].*is not compatible with new inference endpoint \[dense-inference-id\]/ + indices.put_mapping: + index: test-index + body: + properties: + sparse_field: + type: semantic_text + inference_id: dense-inference-id + +--- +"Updating inference_id given dense endpoints with different dimensions should fail": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + dense_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.dense_field.type": semantic_text } + - match: { "test-index.mappings.properties.dense_field.inference_id": dense-inference-id } + - match: { "test-index.mappings.properties.dense_field.model_settings.service": text_embedding_test_service } + + - do: + inference.put: + task_type: text_embedding + inference_id: dense-inference-id-2 + body: > + { + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_model", + "dimensions": 8, + "similarity": "cosine", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + catch: /Cannot update \[semantic_text\] field \[dense_field\].*because inference endpoint \[dense-inference-id\].*is not compatible with new inference endpoint \[dense-inference-id-2\]/ + indices.put_mapping: + index: test-index + body: + properties: + dense_field: + type: semantic_text + inference_id: dense-inference-id-2 + +--- +"Updating inference_id given dense endpoints with different similarity should fail": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + dense_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.dense_field.type": semantic_text } + - match: { "test-index.mappings.properties.dense_field.inference_id": dense-inference-id } + - match: { "test-index.mappings.properties.dense_field.model_settings.service": text_embedding_test_service } + + - do: + inference.put: + task_type: text_embedding + inference_id: dense-inference-id-2 + body: > + { + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_model", + "dimensions": 4, + "similarity": "l2_norm", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + catch: /Cannot update \[semantic_text\] field \[dense_field\].*because inference endpoint \[dense-inference-id\].*is not compatible with new inference endpoint \[dense-inference-id-2\]/ + indices.put_mapping: + index: test-index + body: + properties: + dense_field: + type: semantic_text + inference_id: dense-inference-id-2 + +--- +"Updating inference_id given dense endpoints with different element_type should fail": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + dense_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.dense_field.type": semantic_text } + - match: { "test-index.mappings.properties.dense_field.inference_id": dense-inference-id } + - match: { "test-index.mappings.properties.dense_field.model_settings.service": text_embedding_test_service } + + - do: + inference.put: + task_type: text_embedding + inference_id: dense-inference-id-2 + body: > + { + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_model", + "dimensions": 4, + "similarity": "cosine", + "element_type": "byte", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + catch: /Cannot update \[semantic_text\] field \[dense_field\].*because inference endpoint \[dense-inference-id\].*is not compatible with new inference endpoint \[dense-inference-id-2\]/ + indices.put_mapping: + index: test-index + body: + properties: + dense_field: + type: semantic_text + inference_id: dense-inference-id-2 + +--- +"Updating inference_id to compatible endpoint should succeed given model settings": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id } + - match: { "test-index.mappings.properties.sparse_field.model_settings.service": test_service } + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id-2 + body: > + { + "service": "alternate_sparse_embedding_test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + indices.put_mapping: + index: test-index + body: + properties: + sparse_field: + type: semantic_text + inference_id: sparse-inference-id-2 + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id-2 } + - match: { "test-index.mappings.properties.sparse_field.model_settings.service": alternate_sparse_embedding_test_service } + + # We index another doc to later check inference fields new updated endpoint + - do: + index: + index: test-index + id: doc_2 + body: + sparse_field: "One day they started playing the piano." + refresh: true + + - do: + search: + index: test-index + body: + query: + semantic: + field: "sparse_field" + query: "piano" + _source: + exclude_vectors: false + - match: { hits.total.value: 2 } + - match: { hits.hits.0._source._inference_fields.sparse_field.inference.inference_id: sparse-inference-id-2 } + - match: { hits.hits.0._source._inference_fields.sparse_field.inference.model_settings.service: alternate_sparse_embedding_test_service } + - exists: hits.hits.0._source._inference_fields.sparse_field.inference.chunks.sparse_field.0.embeddings + - match: { hits.hits.1._source._inference_fields.sparse_field.inference.inference_id: sparse-inference-id-2 } + - match: { hits.hits.1._source._inference_fields.sparse_field.inference.model_settings.service: alternate_sparse_embedding_test_service } + - exists: hits.hits.1._source._inference_fields.sparse_field.inference.chunks.sparse_field.0.embeddings diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml index b184423836282..a3fc9e55f85e6 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml @@ -1382,3 +1382,83 @@ setup: - match: { "test-index-options-sparse2.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.prune": true } - match: { "test-index-options-sparse2.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 5.0 } - match: { "test-index-options-sparse2.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.4 } + +--- +"Updating inference_id to compatible endpoint should succeed given model settings": + - requires: + cluster_features: "semantic_text.updatable_inference_id" + reason: inference_id became updatable in 9.3.0 + + # We index a doc to explicitly set model settings + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "This is a story about a cat and a dog." + refresh: true + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id } + - match: { "test-index.mappings.properties.sparse_field.model_settings.service": test_service } + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id-2 + body: > + { + "service": "alternate_sparse_embedding_test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + indices.put_mapping: + index: test-index + body: + properties: + sparse_field: + type: semantic_text + inference_id: sparse-inference-id-2 + + - do: + indices.get_mapping: + index: test-index + - match: { "test-index.mappings.properties.sparse_field.type": semantic_text } + - match: { "test-index.mappings.properties.sparse_field.inference_id": sparse-inference-id-2 } + - match: { "test-index.mappings.properties.sparse_field.model_settings.service": alternate_sparse_embedding_test_service } + + # We index another doc to later check inference fields new updated endpoint + - do: + index: + index: test-index + id: doc_2 + body: + sparse_field: "One day they started playing the piano." + refresh: true + + - do: + search: + index: test-index + body: + query: + semantic: + field: "sparse_field" + query: "piano" + _source: + exclude_vectors: false + - match: { hits.total.value: 2 } + - match: { hits.hits.0._source.sparse_field.inference.inference_id: sparse-inference-id } + - match: { hits.hits.0._source.sparse_field.inference.model_settings.service: test_service } + - exists: hits.hits.0._source.sparse_field.inference.chunks.0.embeddings + - match: { hits.hits.1._source.sparse_field.inference.inference_id: sparse-inference-id-2 } + - match: { hits.hits.1._source.sparse_field.inference.model_settings.service: alternate_sparse_embedding_test_service } + - exists: hits.hits.1._source.sparse_field.inference.chunks.0.embeddings