From 328d0799fd00e132272aae86835c20164d2845a1 Mon Sep 17 00:00:00 2001 From: Mridula Date: Mon, 15 Sep 2025 10:38:06 +0100 Subject: [PATCH 01/28] Defaulting EIS on ELSER --- .../inference/mapper/SemanticTextFieldMapper.java | 15 ++++++++++++++- .../mapper/SemanticTextFieldMapperTests.java | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) 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 5b35c0384ce99..6b586261c7f48 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 @@ -153,9 +153,22 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie public static final String CONTENT_TYPE = "semantic_text"; public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID; + private static final String EIS_ELSER_INFERENCE_ID = ".elser-2-elastic"; public static final float DEFAULT_RESCORE_OVERSAMPLE = 3.0f; + /** + * Determines the preferred ELSER inference ID based on EIS availability. + * Returns .elser-2-elastic (EIS) when available, otherwise falls back to .elser-2-elasticsearch (ML nodes). + * This enables automatic selection of EIS for better performance while maintaining compatibility with on-prem deployments. + */ + private static String getPreferredElserInferenceId(ModelRegistry modelRegistry) { + if (modelRegistry != null && modelRegistry.containsDefaultConfigId(EIS_ELSER_INFERENCE_ID)) { + return EIS_ELSER_INFERENCE_ID; + } + return DEFAULT_ELSER_2_INFERENCE_ID; + } + static final String INDEX_OPTIONS_FIELD = "index_options"; public static final TypeParser parser(Supplier modelRegistry) { @@ -242,7 +255,7 @@ public Builder( INFERENCE_ID_FIELD, false, mapper -> ((SemanticTextFieldType) mapper.fieldType()).inferenceId, - DEFAULT_ELSER_2_INFERENCE_ID + getPreferredElserInferenceId(modelRegistry) ).addValidator(v -> { if (Strings.isEmpty(v)) { throw new IllegalArgumentException( 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 838e4576716ff..4c3bb020a2568 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 @@ -298,6 +298,21 @@ public void testDefaults() throws Exception { assertTrue(fields.isEmpty()); } + public void testDynamicElserDefaultSelection() throws Exception { + final String fieldName = "field"; + final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); + + // Test 1: When EIS is available, should default to .elser-2-elastic + when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(true); + MapperService mapperServiceWithEis = createMapperService(fieldMapping, useLegacyFormat); + assertInferenceEndpoints(mapperServiceWithEis, fieldName, ".elser-2-elastic", ".elser-2-elastic"); + + // Test 2: When EIS is not available, should fallback to .elser-2-elasticsearch + when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(false); + MapperService mapperServiceWithoutEis = createMapperService(fieldMapping, useLegacyFormat); + assertInferenceEndpoints(mapperServiceWithoutEis, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); + } + @Override public void testFieldHasValue() { MappedFieldType fieldType = getMappedFieldType(); From 0b89373d54843baae105c892a03c7e676f92ef7d Mon Sep 17 00:00:00 2001 From: Mridula Date: Mon, 15 Sep 2025 10:52:06 +0100 Subject: [PATCH 02/28] Extended testing --- .../mapper/SemanticTextFieldMapper.java | 11 ++++++++-- .../mapper/SemanticTextFieldMapperTests.java | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) 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 6b586261c7f48..887b111b026fe 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 @@ -163,8 +163,15 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie * This enables automatic selection of EIS for better performance while maintaining compatibility with on-prem deployments. */ private static String getPreferredElserInferenceId(ModelRegistry modelRegistry) { - if (modelRegistry != null && modelRegistry.containsDefaultConfigId(EIS_ELSER_INFERENCE_ID)) { - return EIS_ELSER_INFERENCE_ID; + if (modelRegistry != null) { + try { + if (modelRegistry.containsDefaultConfigId(EIS_ELSER_INFERENCE_ID)) { + return EIS_ELSER_INFERENCE_ID; + } + } catch (Exception e) { + // If ModelRegistry fails, gracefully fallback to ML nodes + logger.debug("Failed to check EIS availability, falling back to ML nodes", e); + } } return DEFAULT_ELSER_2_INFERENCE_ID; } 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 4c3bb020a2568..343d884a032c6 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 @@ -313,6 +313,27 @@ public void testDynamicElserDefaultSelection() throws Exception { assertInferenceEndpoints(mapperServiceWithoutEis, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); } + public void testDynamicElserDefaultSelectionEdgeCases() throws Exception { + final String fieldName = "field"; + final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); + + // Test: ModelRegistry throws exception - should fallback gracefully + when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenThrow(new RuntimeException("Registry error")); + MapperService mapperServiceWithError = createMapperService(fieldMapping, useLegacyFormat); + assertInferenceEndpoints(mapperServiceWithError, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); + } + + public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception { + final String fieldName = "field"; + final String explicitInferenceId = "my-custom-model"; + final XContentBuilder fieldMapping = fieldMapping(b -> b.field("type", "semantic_text").field(INFERENCE_ID_FIELD, explicitInferenceId)); + + // Even when EIS is available, explicit inference_id should take precedence + when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(true); + MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); + assertInferenceEndpoints(mapperService, fieldName, explicitInferenceId, explicitInferenceId); + } + @Override public void testFieldHasValue() { MappedFieldType fieldType = getMappedFieldType(); From 1cc9fd4895ad7778f1122d99b83fd13e63aac908 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 15 Sep 2025 09:59:12 +0000 Subject: [PATCH 03/28] [CI] Auto commit changes from spotless --- .../xpack/inference/mapper/SemanticTextFieldMapperTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 343d884a032c6..d7b3e518b93c2 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 @@ -326,7 +326,9 @@ public void testDynamicElserDefaultSelectionEdgeCases() throws Exception { public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception { final String fieldName = "field"; final String explicitInferenceId = "my-custom-model"; - final XContentBuilder fieldMapping = fieldMapping(b -> b.field("type", "semantic_text").field(INFERENCE_ID_FIELD, explicitInferenceId)); + final XContentBuilder fieldMapping = fieldMapping( + b -> b.field("type", "semantic_text").field(INFERENCE_ID_FIELD, explicitInferenceId) + ); // Even when EIS is available, explicit inference_id should take precedence when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(true); From 7dcd31f1ef3c3e3dcbc5fa920e60ccd32100b650 Mon Sep 17 00:00:00 2001 From: Mridula Date: Wed, 17 Sep 2025 16:25:01 +0100 Subject: [PATCH 04/28] Edited to include the default --- .../xpack/inference/mapper/SemanticTextFieldMapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 887b111b026fe..466d2bfb5b169 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 @@ -108,6 +108,7 @@ import static org.elasticsearch.inference.TaskType.TEXT_EMBEDDING; import static org.elasticsearch.lucene.search.uhighlight.CustomUnifiedHighlighter.MULTIVAL_SEP_CHAR; import static org.elasticsearch.search.SearchService.DEFAULT_SIZE; +import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.CHUNKED_EMBEDDINGS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.CHUNKED_OFFSET_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.CHUNKING_SETTINGS_FIELD; @@ -153,7 +154,7 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie public static final String CONTENT_TYPE = "semantic_text"; public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID; - private static final String EIS_ELSER_INFERENCE_ID = ".elser-2-elastic"; + private static final String EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2; public static final float DEFAULT_RESCORE_OVERSAMPLE = 3.0f; From f19972e90cf33866ee1b5d2ee1b36d69d0654bb4 Mon Sep 17 00:00:00 2001 From: Mridula Date: Wed, 17 Sep 2025 16:31:48 +0100 Subject: [PATCH 05/28] Cleaned up the mapper implementation --- .../inference/mapper/SemanticTextFieldMapper.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) 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 466d2bfb5b169..c70a511a0b943 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 @@ -164,17 +164,10 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie * This enables automatic selection of EIS for better performance while maintaining compatibility with on-prem deployments. */ private static String getPreferredElserInferenceId(ModelRegistry modelRegistry) { - if (modelRegistry != null) { - try { - if (modelRegistry.containsDefaultConfigId(EIS_ELSER_INFERENCE_ID)) { - return EIS_ELSER_INFERENCE_ID; - } - } catch (Exception e) { - // If ModelRegistry fails, gracefully fallback to ML nodes - logger.debug("Failed to check EIS availability, falling back to ML nodes", e); - } + if (modelRegistry != null && modelRegistry.containsDefaultConfigId(EIS_ELSER_INFERENCE_ID)) { + return EIS_ELSER_INFERENCE_ID; } - return DEFAULT_ELSER_2_INFERENCE_ID; + return DEFAULT_ELSER_ID; } static final String INDEX_OPTIONS_FIELD = "index_options"; From 3d58a508ad75f8c323e50028a1175f826ca95e64 Mon Sep 17 00:00:00 2001 From: Mridula Date: Wed, 17 Sep 2025 16:36:49 +0100 Subject: [PATCH 06/28] COmpile issue --- .../inference/services/elastic/ElasticInferenceService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceService.java index 69ae769e36dc4..36bdf9c024ce8 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceService.java @@ -115,7 +115,7 @@ public class ElasticInferenceService extends SenderService { // elser-2 static final String DEFAULT_ELSER_2_MODEL_ID = "elser_model_2"; - static final String DEFAULT_ELSER_ENDPOINT_ID_V2 = defaultEndpointId("elser-2"); + public static final String DEFAULT_ELSER_ENDPOINT_ID_V2 = defaultEndpointId("elser-2"); // multilingual-text-embed static final String DEFAULT_MULTILINGUAL_EMBED_MODEL_ID = "multilingual-embed-v1"; From 394bf99af79de86ea0cffa509a99f67b69372ecd Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 17 Sep 2025 15:47:52 +0000 Subject: [PATCH 07/28] [CI] Auto commit changes from spotless --- .../xpack/inference/mapper/SemanticTextFieldMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c70a511a0b943..7e9cf8afd1574 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 @@ -108,7 +108,6 @@ import static org.elasticsearch.inference.TaskType.TEXT_EMBEDDING; import static org.elasticsearch.lucene.search.uhighlight.CustomUnifiedHighlighter.MULTIVAL_SEP_CHAR; import static org.elasticsearch.search.SearchService.DEFAULT_SIZE; -import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.CHUNKED_EMBEDDINGS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.CHUNKED_OFFSET_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.CHUNKING_SETTINGS_FIELD; @@ -122,6 +121,7 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getOffsetsFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getOriginalTextFieldName; +import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; import static org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalService.DEFAULT_ELSER_ID; /** From c92d2b3e2861275df8a34c168e3cc0c95e7a6ff4 Mon Sep 17 00:00:00 2001 From: Mridula Date: Wed, 1 Oct 2025 00:32:58 +0100 Subject: [PATCH 08/28] Added tests --- .../TestEisMockInferenceServiceExtension.java | 23 ++ .../mock/TestElasticServiceExtension.java | 352 ++++++++++++++++++ .../mock/TestInferenceServicePlugin.java | 16 +- .../qa/SemanticTextEisDefaultIT.java | 32 ++ .../qa/SemanticTextMlFallbackIT.java | 24 ++ ..._semantic_text_default_elser_selection.yml | 122 ++++++ .../30_semantic_text_eis_default.yml | 29 ++ .../31_semantic_text_ml_fallback.yml | 27 ++ 8 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java create mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java create mode 100644 x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java create mode 100644 x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java new file mode 100644 index 0000000000000..030d17461339d --- /dev/null +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.inference.mock; + +import org.elasticsearch.inference.DefaultConfigId; +import org.elasticsearch.plugins.InferencePlugin.TestInferenceServiceExtension; + +import java.util.List; + +import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; + +public class TestEisMockInferenceServiceExtension implements TestInferenceServiceExtension { + + @Override + public List defaultConfigIds() { + // This mocks the presence of the EIS ELSER v2 endpoint, making the test cluster believe it's available. + return List.of(new DefaultConfigId("elser-2", DEFAULT_ELSER_ENDPOINT_ID_V2)); + } +} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java new file mode 100644 index 0000000000000..18513bf2b8412 --- /dev/null +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java @@ -0,0 +1,352 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference.mock; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.action.ActionListener; +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.index.mapper.vectors.DenseVectorFieldMapper; +import org.elasticsearch.inference.ChunkInferenceInput; +import org.elasticsearch.inference.ChunkedInference; +import org.elasticsearch.inference.InferenceServiceConfiguration; +import org.elasticsearch.inference.InferenceServiceExtension; +import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; +import org.elasticsearch.inference.MinimalServiceSettings; +import org.elasticsearch.inference.Model; +import org.elasticsearch.inference.ModelConfigurations; +import org.elasticsearch.inference.ServiceSettings; +import org.elasticsearch.inference.SettingsConfiguration; +import org.elasticsearch.inference.SimilarityMeasure; +import org.elasticsearch.inference.TaskType; +import org.elasticsearch.inference.UnifiedCompletionRequest; +import org.elasticsearch.inference.WeightedToken; +import org.elasticsearch.inference.configuration.SettingsConfigurationFieldType; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.inference.results.ChunkedInferenceEmbedding; +import org.elasticsearch.xpack.core.inference.results.RankedDocsResults; +import org.elasticsearch.xpack.core.inference.results.SparseEmbeddingResults; +import org.elasticsearch.xpack.core.inference.results.TextEmbeddingFloatResults; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Using hardcoded string due to module visibility + +/** + * Mock service extension that simulates Elastic Inference Service (EIS) behavior + * for testing semantic_text field mapping with default endpoints. + */ +public class TestElasticServiceExtension implements InferenceServiceExtension { + + @Override + public List getInferenceServiceFactories() { + return List.of(TestElasticInferenceService::new); + } + + public static class TestElasticInferenceService extends AbstractTestInferenceService { + public static final String NAME = "test_elastic_service"; + + private static final EnumSet supportedTaskTypes = EnumSet.of( + TaskType.SPARSE_EMBEDDING, + TaskType.TEXT_EMBEDDING, + TaskType.RERANK + ); + + public TestElasticInferenceService(InferenceServiceExtension.InferenceServiceFactoryContext context) {} + + @Override + public String name() { + return NAME; + } + + @Override + @SuppressWarnings("unchecked") + public void parseRequestConfig( + String modelId, + TaskType taskType, + Map config, + ActionListener parsedModelListener + ) { + var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); + var serviceSettings = TestServiceSettings.fromMap(serviceSettingsMap); + var secretSettings = TestSecretSettings.fromMap(serviceSettingsMap); + + var taskSettingsMap = getTaskSettingsMap(config); + var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); + + parsedModelListener.onResponse(new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, secretSettings)); + } + + @Override + public InferenceServiceConfiguration getConfiguration() { + return Configuration.get(); + } + + @Override + public EnumSet supportedTaskTypes() { + return supportedTaskTypes; + } + + @Override + public List defaultConfigIds() { + return List.of( + new DefaultConfigId(".elser-2-elastic", MinimalServiceSettings.sparseEmbedding(NAME), this), + new DefaultConfigId( + ".multilingual-embed-v1-elastic", + MinimalServiceSettings.textEmbedding(NAME, 1024, SimilarityMeasure.COSINE, DenseVectorFieldMapper.ElementType.FLOAT), + this + ), + new DefaultConfigId(".rerank-v1-elastic", MinimalServiceSettings.rerank(NAME), this) + ); + } + + @Override + public void infer( + Model model, + @Nullable String query, + @Nullable Boolean returnDocuments, + @Nullable Integer topN, + List input, + boolean stream, + Map taskSettings, + InputType inputType, + TimeValue timeout, + ActionListener listener + ) { + switch (model.getConfigurations().getTaskType()) { + case SPARSE_EMBEDDING -> listener.onResponse(makeSparseResults(input)); + case TEXT_EMBEDDING -> listener.onResponse(makeDenseResults(input)); + case RERANK -> listener.onResponse(makeRerankResults(input)); + default -> listener.onFailure( + new ElasticsearchStatusException( + TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), + RestStatus.BAD_REQUEST + ) + ); + } + } + + @Override + public void unifiedCompletionInfer( + Model model, + UnifiedCompletionRequest request, + TimeValue timeout, + ActionListener listener + ) { + throw new UnsupportedOperationException("unifiedCompletionInfer not supported"); + } + + @Override + public void chunkedInfer( + Model model, + @Nullable String query, + List input, + Map taskSettings, + InputType inputType, + TimeValue timeout, + ActionListener> listener + ) { + switch (model.getConfigurations().getTaskType()) { + case SPARSE_EMBEDDING -> listener.onResponse(makeChunkedSparseResults(input)); + case TEXT_EMBEDDING -> listener.onResponse(makeChunkedDenseResults(input)); + case RERANK -> listener.onFailure( + new ElasticsearchStatusException( + "Chunked inference is not supported for rerank task type", + RestStatus.BAD_REQUEST + ) + ); + default -> listener.onFailure( + new ElasticsearchStatusException( + TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), + RestStatus.BAD_REQUEST + ) + ); + } + } + + private SparseEmbeddingResults makeSparseResults(List input) { + var embeddings = new ArrayList(); + for (int i = 0; i < input.size(); i++) { + var tokens = new ArrayList(); + for (int j = 0; j < 5; j++) { + tokens.add(new WeightedToken("feature_" + j, generateEmbedding(input.get(i), j))); + } + embeddings.add(new SparseEmbeddingResults.Embedding(tokens, false)); + } + return new SparseEmbeddingResults(embeddings); + } + + private TextEmbeddingFloatResults makeDenseResults(List input) { + var embeddings = new ArrayList(); + for (String text : input) { + float[] embedding = new float[1024]; + for (int i = 0; i < embedding.length; i++) { + embedding[i] = generateEmbedding(text, i); + } + embeddings.add(new TextEmbeddingFloatResults.Embedding(embedding)); + } + return new TextEmbeddingFloatResults(embeddings); + } + + private List makeChunkedSparseResults(List inputs) { + List results = new ArrayList<>(); + for (ChunkInferenceInput chunkInferenceInput : inputs) { + List chunkedInput = chunkInputs(chunkInferenceInput); + List chunks = chunkedInput.stream().map(c -> { + var tokens = new ArrayList(); + for (int i = 0; i < 5; i++) { + tokens.add(new WeightedToken("feature_" + i, generateEmbedding(c.input(), i))); + } + var embeddings = new SparseEmbeddingResults.Embedding(tokens, false); + return new SparseEmbeddingResults.Chunk(embeddings, new ChunkedInference.TextOffset(c.startOffset(), c.endOffset())); + }).toList(); + ChunkedInferenceEmbedding chunkedInferenceEmbedding = new ChunkedInferenceEmbedding(chunks); + results.add(chunkedInferenceEmbedding); + } + return results; + } + + private List makeChunkedDenseResults(List inputs) { + List results = new ArrayList<>(); + for (ChunkInferenceInput chunkInferenceInput : inputs) { + List chunkedInput = chunkInputs(chunkInferenceInput); + List chunks = chunkedInput.stream().map(c -> { + float[] embedding = new float[1024]; + for (int i = 0; i < embedding.length; i++) { + embedding[i] = generateEmbedding(c.input(), i); + } + var embeddings = new TextEmbeddingFloatResults.Embedding(embedding); + return new TextEmbeddingFloatResults.Chunk(embeddings, new ChunkedInference.TextOffset(c.startOffset(), c.endOffset())); + }).toList(); + ChunkedInferenceEmbedding chunkedInferenceEmbedding = new ChunkedInferenceEmbedding(chunks); + results.add(chunkedInferenceEmbedding); + } + return results; + } + + protected ServiceSettings getServiceSettingsFromMap(Map serviceSettingsMap) { + return TestServiceSettings.fromMap(serviceSettingsMap); + } + + private RankedDocsResults makeRerankResults(List input) { + var rankedDocs = new ArrayList(); + for (int i = 0; i < input.size(); i++) { + float relevanceScore = 1.0f - (i * 0.1f); // Decreasing relevance scores + rankedDocs.add(new RankedDocsResults.RankedDoc(i, Math.max(0.1f, relevanceScore), input.get(i))); + } + return new RankedDocsResults(rankedDocs); + } + + private static float generateEmbedding(String input, int position) { + return Math.abs(input.hashCode()) + 1 + position; + } + + public static class Configuration { + public static InferenceServiceConfiguration get() { + return configuration.getOrCompute(); + } + + private static final LazyInitializable configuration = new LazyInitializable<>( + () -> { + var configurationMap = new HashMap(); + + configurationMap.put( + "model", + new SettingsConfiguration.Builder(supportedTaskTypes).setDescription("") + .setLabel("Model") + .setRequired(true) + .setSensitive(false) + .setType(SettingsConfigurationFieldType.STRING) + .build() + ); + + return new InferenceServiceConfiguration.Builder().setService(NAME) + .setName(NAME) + .setTaskTypes(supportedTaskTypes) + .setConfigurations(configurationMap) + .build(); + } + ); + } + } + + public record TestServiceSettings(String model) implements ServiceSettings { + + static final String NAME = "test_elastic_service_settings"; + + public static TestServiceSettings fromMap(Map map) { + ValidationException validationException = new ValidationException(); + + String model = (String) map.remove("model"); + if (model == null) { + validationException.addValidationError("missing model"); + } + + if (validationException.validationErrors().isEmpty() == false) { + throw validationException; + } + + return new TestServiceSettings(model); + } + + public TestServiceSettings(StreamInput in) throws IOException { + this(in.readString()); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("model", model); + builder.endObject(); + return builder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersion.current(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(model); + } + + @Override + public String modelId() { + return model; + } + + @Override + public ToXContentObject getFilteredXContentObject() { + return (builder, params) -> { + builder.startObject(); + builder.field("model", model); + builder.endObject(); + return builder; + }; + } + } +} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java index 4cfc7e388a911..552ce7107fcb5 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java @@ -15,7 +15,9 @@ import java.util.List; -public class TestInferenceServicePlugin extends Plugin { +import org.elasticsearch.plugins.InferencePlugin; + +public class TestInferenceServicePlugin extends Plugin implements InferencePlugin { @Override public List getNamedWriteables() { @@ -62,4 +64,16 @@ public List getNamedWriteables() { ) ); } + + @Override + public List getTestInferenceServiceExtensions() { + return List.of( + new TestCompletionServiceExtension(), + new TestDenseInferenceServiceExtension(), + new TestRerankingServiceExtension(), + new TestSparseInferenceServiceExtension(), + new TestStreamingCompletionServiceExtension(), + new TestEisMockInferenceServiceExtension() + ); + } } diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java new file mode 100644 index 0000000000000..bd82bc9474ffa --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.inference.qa; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; +import org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin; + +import java.util.Collection; + +public class SemanticTextEisDefaultIT extends ESClientYamlSuiteTestCase { + + public SemanticTextEisDefaultIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return createParameters(); + } + + @Override + protected Collection> getTestRestPlugins() { + return java.util.List.of(TestInferenceServicePlugin.class); + } +} diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java new file mode 100644 index 0000000000000..fcad8eaa43d56 --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.inference.qa; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; + +public class SemanticTextMlFallbackIT extends ESClientYamlSuiteTestCase { + + public SemanticTextMlFallbackIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return createParameters(); + } +} diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml new file mode 100644 index 0000000000000..9ac418eb1a43f --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml @@ -0,0 +1,122 @@ +setup: + - requires: + cluster_features: "gte_v8.15.0" + reason: semantic_text introduced in 8.15.0 + + - do: + inference.put: + task_type: sparse_embedding + inference_id: test-elser-service + body: > + { + "service": "test_elastic_service", + "service_settings": { + "model": "elser_model_2" + } + } + +--- +"Test semantic_text field defaults to EIS ELSER when available": + + - do: + indices.create: + index: test_index + body: + mappings: + properties: + my_semantic_field: + type: semantic_text + + - do: + indices.get_mapping: + index: test_index + + - match: { test_index.mappings.properties.my_semantic_field.type: "semantic_text" } + # Should default to .elser-2-elastic when EIS is available (mocked by test_elastic_service) + - match: { test_index.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } + +--- +"Test semantic_text field with explicit inference_id": + + - do: + indices.create: + index: test_index_explicit + body: + mappings: + properties: + my_semantic_field: + type: semantic_text + inference_id: test-elser-service + + - do: + indices.get_mapping: + index: test_index_explicit + + - match: { test_index_explicit.mappings.properties.my_semantic_field.type: "semantic_text" } + - match: { test_index_explicit.mappings.properties.my_semantic_field.inference_id: "test-elser-service" } + +--- +"Test semantic_text field with search_inference_id": + + - do: + indices.create: + index: test_index_search + body: + mappings: + properties: + my_semantic_field: + type: semantic_text + search_inference_id: test-elser-service + + - do: + indices.get_mapping: + index: test_index_search + + - match: { test_index_search.mappings.properties.my_semantic_field.type: "semantic_text" } + # Should still default indexing to EIS endpoint + - match: { test_index_search.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } + # But use explicit search endpoint + - match: { test_index_search.mappings.properties.my_semantic_field.search_inference_id: "test-elser-service" } + +--- +"Test semantic_text field mapping creation and document indexing": + + - do: + indices.create: + index: test_index_docs + body: + mappings: + properties: + my_semantic_field: + type: semantic_text + settings: + number_of_shards: 1 + number_of_replicas: 0 + + # Index a document + - do: + index: + index: test_index_docs + id: doc1 + body: + my_semantic_field: "Hello world semantic search" + + - do: + indices.refresh: + index: test_index_docs + + # Verify document was indexed + - do: + get: + index: test_index_docs + id: doc1 + + - match: { _source.my_semantic_field: "Hello world semantic search" } + + # Verify the mapping was created correctly + - do: + indices.get_mapping: + index: test_index_docs + + - match: { test_index_docs.mappings.properties.my_semantic_field.type: "semantic_text" } + - match: { test_index_docs.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } \ No newline at end of file diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml new file mode 100644 index 0000000000000..3f8cc6f6e218c --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml @@ -0,0 +1,29 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# +--- +"Test semantic_text defaults to EIS when available": + - skip: + features: [ test_plugin ] + + - do: + indices.create: + index: test_semantic_text_eis_default + body: + mappings: + properties: + my_semantic_field: + type: "semantic_text" + + - do: + indices.get_mapping: + index: test_semantic_text_eis_default + + - match: { test_semantic_text_eis_default.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } + + - do: + indices.delete: + index: test_semantic_text_eis_default diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml new file mode 100644 index 0000000000000..4501cf11ef2c5 --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml @@ -0,0 +1,27 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# +--- +"Test semantic_text falls back to ML node model when EIS is not available": + + - do: + indices.create: + index: test_semantic_text_ml_fallback + body: + mappings: + properties: + my_semantic_field: + type: "semantic_text" + + - do: + indices.get_mapping: + index: test_semantic_text_ml_fallback + + - match: { test_semantic_text_ml_fallback.mappings.properties.my_semantic_field.inference_id: ".elser-2-elasticsearch" } + + - do: + indices.delete: + index: test_semantic_text_ml_fallback From 1301a4304643eeb704dbf02e57b3aa13f35baff8 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 30 Sep 2025 23:39:46 +0000 Subject: [PATCH 09/28] [CI] Auto commit changes from spotless --- .../xpack/inference/mock/TestElasticServiceExtension.java | 5 +---- .../xpack/inference/mock/TestInferenceServicePlugin.java | 3 +-- .../xpack/inference/qa/SemanticTextEisDefaultIT.java | 1 + .../xpack/inference/qa/SemanticTextMlFallbackIT.java | 1 + 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java index 18513bf2b8412..45f3547e0656d 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java @@ -168,10 +168,7 @@ public void chunkedInfer( case SPARSE_EMBEDDING -> listener.onResponse(makeChunkedSparseResults(input)); case TEXT_EMBEDDING -> listener.onResponse(makeChunkedDenseResults(input)); case RERANK -> listener.onFailure( - new ElasticsearchStatusException( - "Chunked inference is not supported for rerank task type", - RestStatus.BAD_REQUEST - ) + new ElasticsearchStatusException("Chunked inference is not supported for rerank task type", RestStatus.BAD_REQUEST) ); default -> listener.onFailure( new ElasticsearchStatusException( diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java index 552ce7107fcb5..d771d23bdec95 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java @@ -11,12 +11,11 @@ import org.elasticsearch.inference.SecretSettings; import org.elasticsearch.inference.ServiceSettings; import org.elasticsearch.inference.TaskSettings; +import org.elasticsearch.plugins.InferencePlugin; import org.elasticsearch.plugins.Plugin; import java.util.List; -import org.elasticsearch.plugins.InferencePlugin; - public class TestInferenceServicePlugin extends Plugin implements InferencePlugin { @Override diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java index bd82bc9474ffa..837aeafebd04d 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java +++ b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java @@ -8,6 +8,7 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; import org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin; diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java index fcad8eaa43d56..8c55a5d5ecace 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java +++ b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java @@ -8,6 +8,7 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; From 9f662ce5f977137be0142b8b3ec7b559e25d2923 Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 3 Oct 2025 00:24:50 +0100 Subject: [PATCH 10/28] Refactored the variable names --- .../inference/mapper/SemanticTextFieldMapper.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 c03f4724f5c40..30d838c5fd7ec 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 @@ -152,8 +152,8 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie ); public static final String CONTENT_TYPE = "semantic_text"; - public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID; - private static final String EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2; + public static final String DEFAULT_FALLBACK_ELSER_INFERENCE_ID = DEFAULT_ELSER_ID; + private static final String DEFAULT_EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2; public static final float DEFAULT_RESCORE_OVERSAMPLE = 3.0f; @@ -163,10 +163,10 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie * This enables automatic selection of EIS for better performance while maintaining compatibility with on-prem deployments. */ private static String getPreferredElserInferenceId(ModelRegistry modelRegistry) { - if (modelRegistry != null && modelRegistry.containsDefaultConfigId(EIS_ELSER_INFERENCE_ID)) { - return EIS_ELSER_INFERENCE_ID; + if (modelRegistry != null && modelRegistry.containsDefaultConfigId(DEFAULT_EIS_ELSER_INFERENCE_ID)) { + return DEFAULT_EIS_ELSER_INFERENCE_ID; } - return DEFAULT_ELSER_ID; + return DEFAULT_FALLBACK_ELSER_INFERENCE_ID; } static final String INDEX_OPTIONS_FIELD = "index_options"; From 904a242b362e18f121b8a763dbd3cfd63fe8ca6e Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 3 Oct 2025 00:39:48 +0100 Subject: [PATCH 11/28] Cleanup done --- .../TestEisMockInferenceServiceExtension.java | 23 -- .../mock/TestElasticServiceExtension.java | 349 ------------------ .../mock/TestInferenceServicePlugin.java | 15 +- .../mapper/SemanticTextFieldMapperTests.java | 24 +- 4 files changed, 14 insertions(+), 397 deletions(-) delete mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java delete mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java deleted file mode 100644 index 030d17461339d..0000000000000 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestEisMockInferenceServiceExtension.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.inference.mock; - -import org.elasticsearch.inference.DefaultConfigId; -import org.elasticsearch.plugins.InferencePlugin.TestInferenceServiceExtension; - -import java.util.List; - -import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; - -public class TestEisMockInferenceServiceExtension implements TestInferenceServiceExtension { - - @Override - public List defaultConfigIds() { - // This mocks the presence of the EIS ELSER v2 endpoint, making the test cluster believe it's available. - return List.of(new DefaultConfigId("elser-2", DEFAULT_ELSER_ENDPOINT_ID_V2)); - } -} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java deleted file mode 100644 index 45f3547e0656d..0000000000000 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestElasticServiceExtension.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference.mock; - -import org.elasticsearch.ElasticsearchStatusException; -import org.elasticsearch.TransportVersion; -import org.elasticsearch.action.ActionListener; -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.index.mapper.vectors.DenseVectorFieldMapper; -import org.elasticsearch.inference.ChunkInferenceInput; -import org.elasticsearch.inference.ChunkedInference; -import org.elasticsearch.inference.InferenceServiceConfiguration; -import org.elasticsearch.inference.InferenceServiceExtension; -import org.elasticsearch.inference.InferenceServiceResults; -import org.elasticsearch.inference.InputType; -import org.elasticsearch.inference.MinimalServiceSettings; -import org.elasticsearch.inference.Model; -import org.elasticsearch.inference.ModelConfigurations; -import org.elasticsearch.inference.ServiceSettings; -import org.elasticsearch.inference.SettingsConfiguration; -import org.elasticsearch.inference.SimilarityMeasure; -import org.elasticsearch.inference.TaskType; -import org.elasticsearch.inference.UnifiedCompletionRequest; -import org.elasticsearch.inference.WeightedToken; -import org.elasticsearch.inference.configuration.SettingsConfigurationFieldType; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xpack.core.inference.results.ChunkedInferenceEmbedding; -import org.elasticsearch.xpack.core.inference.results.RankedDocsResults; -import org.elasticsearch.xpack.core.inference.results.SparseEmbeddingResults; -import org.elasticsearch.xpack.core.inference.results.TextEmbeddingFloatResults; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -// Using hardcoded string due to module visibility - -/** - * Mock service extension that simulates Elastic Inference Service (EIS) behavior - * for testing semantic_text field mapping with default endpoints. - */ -public class TestElasticServiceExtension implements InferenceServiceExtension { - - @Override - public List getInferenceServiceFactories() { - return List.of(TestElasticInferenceService::new); - } - - public static class TestElasticInferenceService extends AbstractTestInferenceService { - public static final String NAME = "test_elastic_service"; - - private static final EnumSet supportedTaskTypes = EnumSet.of( - TaskType.SPARSE_EMBEDDING, - TaskType.TEXT_EMBEDDING, - TaskType.RERANK - ); - - public TestElasticInferenceService(InferenceServiceExtension.InferenceServiceFactoryContext context) {} - - @Override - public String name() { - return NAME; - } - - @Override - @SuppressWarnings("unchecked") - public void parseRequestConfig( - String modelId, - TaskType taskType, - Map config, - ActionListener parsedModelListener - ) { - var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); - var serviceSettings = TestServiceSettings.fromMap(serviceSettingsMap); - var secretSettings = TestSecretSettings.fromMap(serviceSettingsMap); - - var taskSettingsMap = getTaskSettingsMap(config); - var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); - - parsedModelListener.onResponse(new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, secretSettings)); - } - - @Override - public InferenceServiceConfiguration getConfiguration() { - return Configuration.get(); - } - - @Override - public EnumSet supportedTaskTypes() { - return supportedTaskTypes; - } - - @Override - public List defaultConfigIds() { - return List.of( - new DefaultConfigId(".elser-2-elastic", MinimalServiceSettings.sparseEmbedding(NAME), this), - new DefaultConfigId( - ".multilingual-embed-v1-elastic", - MinimalServiceSettings.textEmbedding(NAME, 1024, SimilarityMeasure.COSINE, DenseVectorFieldMapper.ElementType.FLOAT), - this - ), - new DefaultConfigId(".rerank-v1-elastic", MinimalServiceSettings.rerank(NAME), this) - ); - } - - @Override - public void infer( - Model model, - @Nullable String query, - @Nullable Boolean returnDocuments, - @Nullable Integer topN, - List input, - boolean stream, - Map taskSettings, - InputType inputType, - TimeValue timeout, - ActionListener listener - ) { - switch (model.getConfigurations().getTaskType()) { - case SPARSE_EMBEDDING -> listener.onResponse(makeSparseResults(input)); - case TEXT_EMBEDDING -> listener.onResponse(makeDenseResults(input)); - case RERANK -> listener.onResponse(makeRerankResults(input)); - default -> listener.onFailure( - new ElasticsearchStatusException( - TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), - RestStatus.BAD_REQUEST - ) - ); - } - } - - @Override - public void unifiedCompletionInfer( - Model model, - UnifiedCompletionRequest request, - TimeValue timeout, - ActionListener listener - ) { - throw new UnsupportedOperationException("unifiedCompletionInfer not supported"); - } - - @Override - public void chunkedInfer( - Model model, - @Nullable String query, - List input, - Map taskSettings, - InputType inputType, - TimeValue timeout, - ActionListener> listener - ) { - switch (model.getConfigurations().getTaskType()) { - case SPARSE_EMBEDDING -> listener.onResponse(makeChunkedSparseResults(input)); - case TEXT_EMBEDDING -> listener.onResponse(makeChunkedDenseResults(input)); - case RERANK -> listener.onFailure( - new ElasticsearchStatusException("Chunked inference is not supported for rerank task type", RestStatus.BAD_REQUEST) - ); - default -> listener.onFailure( - new ElasticsearchStatusException( - TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), - RestStatus.BAD_REQUEST - ) - ); - } - } - - private SparseEmbeddingResults makeSparseResults(List input) { - var embeddings = new ArrayList(); - for (int i = 0; i < input.size(); i++) { - var tokens = new ArrayList(); - for (int j = 0; j < 5; j++) { - tokens.add(new WeightedToken("feature_" + j, generateEmbedding(input.get(i), j))); - } - embeddings.add(new SparseEmbeddingResults.Embedding(tokens, false)); - } - return new SparseEmbeddingResults(embeddings); - } - - private TextEmbeddingFloatResults makeDenseResults(List input) { - var embeddings = new ArrayList(); - for (String text : input) { - float[] embedding = new float[1024]; - for (int i = 0; i < embedding.length; i++) { - embedding[i] = generateEmbedding(text, i); - } - embeddings.add(new TextEmbeddingFloatResults.Embedding(embedding)); - } - return new TextEmbeddingFloatResults(embeddings); - } - - private List makeChunkedSparseResults(List inputs) { - List results = new ArrayList<>(); - for (ChunkInferenceInput chunkInferenceInput : inputs) { - List chunkedInput = chunkInputs(chunkInferenceInput); - List chunks = chunkedInput.stream().map(c -> { - var tokens = new ArrayList(); - for (int i = 0; i < 5; i++) { - tokens.add(new WeightedToken("feature_" + i, generateEmbedding(c.input(), i))); - } - var embeddings = new SparseEmbeddingResults.Embedding(tokens, false); - return new SparseEmbeddingResults.Chunk(embeddings, new ChunkedInference.TextOffset(c.startOffset(), c.endOffset())); - }).toList(); - ChunkedInferenceEmbedding chunkedInferenceEmbedding = new ChunkedInferenceEmbedding(chunks); - results.add(chunkedInferenceEmbedding); - } - return results; - } - - private List makeChunkedDenseResults(List inputs) { - List results = new ArrayList<>(); - for (ChunkInferenceInput chunkInferenceInput : inputs) { - List chunkedInput = chunkInputs(chunkInferenceInput); - List chunks = chunkedInput.stream().map(c -> { - float[] embedding = new float[1024]; - for (int i = 0; i < embedding.length; i++) { - embedding[i] = generateEmbedding(c.input(), i); - } - var embeddings = new TextEmbeddingFloatResults.Embedding(embedding); - return new TextEmbeddingFloatResults.Chunk(embeddings, new ChunkedInference.TextOffset(c.startOffset(), c.endOffset())); - }).toList(); - ChunkedInferenceEmbedding chunkedInferenceEmbedding = new ChunkedInferenceEmbedding(chunks); - results.add(chunkedInferenceEmbedding); - } - return results; - } - - protected ServiceSettings getServiceSettingsFromMap(Map serviceSettingsMap) { - return TestServiceSettings.fromMap(serviceSettingsMap); - } - - private RankedDocsResults makeRerankResults(List input) { - var rankedDocs = new ArrayList(); - for (int i = 0; i < input.size(); i++) { - float relevanceScore = 1.0f - (i * 0.1f); // Decreasing relevance scores - rankedDocs.add(new RankedDocsResults.RankedDoc(i, Math.max(0.1f, relevanceScore), input.get(i))); - } - return new RankedDocsResults(rankedDocs); - } - - private static float generateEmbedding(String input, int position) { - return Math.abs(input.hashCode()) + 1 + position; - } - - public static class Configuration { - public static InferenceServiceConfiguration get() { - return configuration.getOrCompute(); - } - - private static final LazyInitializable configuration = new LazyInitializable<>( - () -> { - var configurationMap = new HashMap(); - - configurationMap.put( - "model", - new SettingsConfiguration.Builder(supportedTaskTypes).setDescription("") - .setLabel("Model") - .setRequired(true) - .setSensitive(false) - .setType(SettingsConfigurationFieldType.STRING) - .build() - ); - - return new InferenceServiceConfiguration.Builder().setService(NAME) - .setName(NAME) - .setTaskTypes(supportedTaskTypes) - .setConfigurations(configurationMap) - .build(); - } - ); - } - } - - public record TestServiceSettings(String model) implements ServiceSettings { - - static final String NAME = "test_elastic_service_settings"; - - public static TestServiceSettings fromMap(Map map) { - ValidationException validationException = new ValidationException(); - - String model = (String) map.remove("model"); - if (model == null) { - validationException.addValidationError("missing model"); - } - - if (validationException.validationErrors().isEmpty() == false) { - throw validationException; - } - - return new TestServiceSettings(model); - } - - public TestServiceSettings(StreamInput in) throws IOException { - this(in.readString()); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("model", model); - builder.endObject(); - return builder; - } - - @Override - public String getWriteableName() { - return NAME; - } - - @Override - public TransportVersion getMinimalSupportedVersion() { - return TransportVersion.current(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(model); - } - - @Override - public String modelId() { - return model; - } - - @Override - public ToXContentObject getFilteredXContentObject() { - return (builder, params) -> { - builder.startObject(); - builder.field("model", model); - builder.endObject(); - return builder; - }; - } - } -} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java index d771d23bdec95..4cfc7e388a911 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java @@ -11,12 +11,11 @@ import org.elasticsearch.inference.SecretSettings; import org.elasticsearch.inference.ServiceSettings; import org.elasticsearch.inference.TaskSettings; -import org.elasticsearch.plugins.InferencePlugin; import org.elasticsearch.plugins.Plugin; import java.util.List; -public class TestInferenceServicePlugin extends Plugin implements InferencePlugin { +public class TestInferenceServicePlugin extends Plugin { @Override public List getNamedWriteables() { @@ -63,16 +62,4 @@ public List getNamedWriteables() { ) ); } - - @Override - public List getTestInferenceServiceExtensions() { - return List.of( - new TestCompletionServiceExtension(), - new TestDenseInferenceServiceExtension(), - new TestRerankingServiceExtension(), - new TestSparseInferenceServiceExtension(), - new TestStreamingCompletionServiceExtension(), - new TestEisMockInferenceServiceExtension() - ); - } } 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 d7b3e518b93c2..ddd5ea2df4227 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 @@ -80,6 +80,7 @@ import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.inference.InferencePlugin; +import org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin; import org.elasticsearch.xpack.inference.model.TestModel; import org.elasticsearch.xpack.inference.registry.ModelRegistry; import org.junit.After; @@ -108,13 +109,14 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.TEXT_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getChunksFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName; -import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_ELSER_2_INFERENCE_ID; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_RESCORE_OVERSAMPLE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.INDEX_OPTIONS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.UNSUPPORTED_INDEX_MESSAGE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettings; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettingsOtherThan; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.randomSemanticText; +import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -164,7 +166,7 @@ protected Collection getPlugins() { protected Supplier getModelRegistry() { return () -> globalModelRegistry; } - }, new XPackClientPlugin()); + }, new XPackClientPlugin(), new TestInferenceServicePlugin()); } private MapperService createMapperService(XContentBuilder mappings, boolean useLegacyFormat) throws IOException { @@ -221,7 +223,7 @@ protected void minimalMapping(XContentBuilder b) throws IOException { @Override protected void metaMapping(XContentBuilder b) throws IOException { super.metaMapping(b); - b.field(INFERENCE_ID_FIELD, DEFAULT_ELSER_2_INFERENCE_ID); + b.field(INFERENCE_ID_FIELD, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); } @Override @@ -289,7 +291,7 @@ public void testDefaults() throws Exception { DocumentMapper mapper = mapperService.documentMapper(); assertEquals(Strings.toString(expectedMapping), mapper.mappingSource().toString()); assertSemanticTextField(mapperService, fieldName, false, null, null); - assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); ParsedDocument doc1 = mapper.parse(source(this::writeField)); List fields = doc1.rootDoc().getFields("field"); @@ -303,14 +305,14 @@ public void testDynamicElserDefaultSelection() throws Exception { final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); // Test 1: When EIS is available, should default to .elser-2-elastic - when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(true); + when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenReturn(true); MapperService mapperServiceWithEis = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperServiceWithEis, fieldName, ".elser-2-elastic", ".elser-2-elastic"); + assertInferenceEndpoints(mapperServiceWithEis, fieldName, DEFAULT_ELSER_ENDPOINT_ID_V2, DEFAULT_ELSER_ENDPOINT_ID_V2); // Test 2: When EIS is not available, should fallback to .elser-2-elasticsearch - when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(false); + when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenReturn(false); MapperService mapperServiceWithoutEis = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperServiceWithoutEis, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); + assertInferenceEndpoints(mapperServiceWithoutEis, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); } public void testDynamicElserDefaultSelectionEdgeCases() throws Exception { @@ -318,9 +320,9 @@ public void testDynamicElserDefaultSelectionEdgeCases() throws Exception { final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); // Test: ModelRegistry throws exception - should fallback gracefully - when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenThrow(new RuntimeException("Registry error")); + when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenThrow(new RuntimeException("Registry error")); MapperService mapperServiceWithError = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperServiceWithError, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID); + assertInferenceEndpoints(mapperServiceWithError, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); } public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception { @@ -331,7 +333,7 @@ public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception ); // Even when EIS is available, explicit inference_id should take precedence - when(globalModelRegistry.containsDefaultConfigId(".elser-2-elastic")).thenReturn(true); + when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenReturn(true); MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); assertInferenceEndpoints(mapperService, fieldName, explicitInferenceId, explicitInferenceId); } From 6355bed74fc3a90b7a4c50b4960e1c818aaae8a6 Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 3 Oct 2025 00:52:47 +0100 Subject: [PATCH 12/28] Removed unnecessary files --- .../mapper/SemanticTextFieldMapperTests.java | 47 +++++++ .../qa/SemanticTextEisDefaultIT.java | 33 ----- .../qa/SemanticTextMlFallbackIT.java | 25 ---- ..._semantic_text_default_elser_selection.yml | 122 ------------------ .../30_semantic_text_eis_default.yml | 29 ----- .../31_semantic_text_ml_fallback.yml | 27 ---- 6 files changed, 47 insertions(+), 236 deletions(-) delete mode 100644 x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java delete mode 100644 x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java delete mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml delete mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml delete mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml 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 ddd5ea2df4227..6b1c510265c5e 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 @@ -62,6 +62,7 @@ import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; import org.elasticsearch.inference.ChunkingSettings; +import org.elasticsearch.inference.InferenceService; import org.elasticsearch.inference.MinimalServiceSettings; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.SimilarityMeasure; @@ -83,6 +84,7 @@ import org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin; import org.elasticsearch.xpack.inference.model.TestModel; import org.elasticsearch.xpack.inference.registry.ModelRegistry; +import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService; import org.junit.After; import org.junit.AssumptionViolatedException; import org.junit.Before; @@ -122,6 +124,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class SemanticTextFieldMapperTests extends MapperTestCase { @@ -147,6 +150,7 @@ public boolean localNodeMaster() { return false; } }); + registerDefaultEisEndpoint(); } @After @@ -169,6 +173,16 @@ protected Supplier getModelRegistry() { }, new XPackClientPlugin(), new TestInferenceServicePlugin()); } + private void registerDefaultEisEndpoint() { + globalModelRegistry.putDefaultIdIfAbsent( + new InferenceService.DefaultConfigId( + DEFAULT_ELSER_ENDPOINT_ID_V2, + MinimalServiceSettings.sparseEmbedding(ElasticInferenceService.NAME), + mock(InferenceService.class) + ) + ); + } + private MapperService createMapperService(XContentBuilder mappings, boolean useLegacyFormat) throws IOException { IndexVersion indexVersion = SemanticInferenceMetadataFieldsMapperTests.getRandomCompatibleIndexVersion(useLegacyFormat); return createMapperService(mappings, useLegacyFormat, indexVersion, indexVersion); @@ -300,6 +314,39 @@ public void testDefaults() throws Exception { assertTrue(fields.isEmpty()); } + public void testDefaultInferenceIdUsesEisWhenAvailable() throws Exception { + final String fieldName = "field"; + final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); + + MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_ENDPOINT_ID_V2, DEFAULT_ELSER_ENDPOINT_ID_V2); + DocumentMapper mapper = mapperService.documentMapper(); + assertThat(mapper.mappingSource().toString(), containsString("\"inference_id\":\"" + DEFAULT_ELSER_ENDPOINT_ID_V2 + "\"")); + } + + public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception { + final String fieldName = "field"; + final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); + + globalModelRegistry.clearDefaultIds(); + try { + MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); + assertInferenceEndpoints( + mapperService, + fieldName, + DEFAULT_FALLBACK_ELSER_INFERENCE_ID, + DEFAULT_FALLBACK_ELSER_INFERENCE_ID + ); + DocumentMapper mapper = mapperService.documentMapper(); + assertThat( + mapper.mappingSource().toString(), + containsString("\"inference_id\":\"" + DEFAULT_FALLBACK_ELSER_INFERENCE_ID + "\"") + ); + } finally { + registerDefaultEisEndpoint(); + } + } + public void testDynamicElserDefaultSelection() throws Exception { final String fieldName = "field"; final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java deleted file mode 100644 index 837aeafebd04d..0000000000000 --- a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextEisDefaultIT.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.inference.qa; - -import com.carrotsearch.randomizedtesting.annotations.Name; -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; - -import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; -import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; -import org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin; - -import java.util.Collection; - -public class SemanticTextEisDefaultIT extends ESClientYamlSuiteTestCase { - - public SemanticTextEisDefaultIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { - super(testCandidate); - } - - @ParametersFactory - public static Iterable parameters() throws Exception { - return createParameters(); - } - - @Override - protected Collection> getTestRestPlugins() { - return java.util.List.of(TestInferenceServicePlugin.class); - } -} diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java deleted file mode 100644 index 8c55a5d5ecace..0000000000000 --- a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/qa/SemanticTextMlFallbackIT.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.inference.qa; - -import com.carrotsearch.randomizedtesting.annotations.Name; -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; - -import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; -import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; - -public class SemanticTextMlFallbackIT extends ESClientYamlSuiteTestCase { - - public SemanticTextMlFallbackIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { - super(testCandidate); - } - - @ParametersFactory - public static Iterable parameters() throws Exception { - return createParameters(); - } -} diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml deleted file mode 100644 index 9ac418eb1a43f..0000000000000 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/15_semantic_text_default_elser_selection.yml +++ /dev/null @@ -1,122 +0,0 @@ -setup: - - requires: - cluster_features: "gte_v8.15.0" - reason: semantic_text introduced in 8.15.0 - - - do: - inference.put: - task_type: sparse_embedding - inference_id: test-elser-service - body: > - { - "service": "test_elastic_service", - "service_settings": { - "model": "elser_model_2" - } - } - ---- -"Test semantic_text field defaults to EIS ELSER when available": - - - do: - indices.create: - index: test_index - body: - mappings: - properties: - my_semantic_field: - type: semantic_text - - - do: - indices.get_mapping: - index: test_index - - - match: { test_index.mappings.properties.my_semantic_field.type: "semantic_text" } - # Should default to .elser-2-elastic when EIS is available (mocked by test_elastic_service) - - match: { test_index.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } - ---- -"Test semantic_text field with explicit inference_id": - - - do: - indices.create: - index: test_index_explicit - body: - mappings: - properties: - my_semantic_field: - type: semantic_text - inference_id: test-elser-service - - - do: - indices.get_mapping: - index: test_index_explicit - - - match: { test_index_explicit.mappings.properties.my_semantic_field.type: "semantic_text" } - - match: { test_index_explicit.mappings.properties.my_semantic_field.inference_id: "test-elser-service" } - ---- -"Test semantic_text field with search_inference_id": - - - do: - indices.create: - index: test_index_search - body: - mappings: - properties: - my_semantic_field: - type: semantic_text - search_inference_id: test-elser-service - - - do: - indices.get_mapping: - index: test_index_search - - - match: { test_index_search.mappings.properties.my_semantic_field.type: "semantic_text" } - # Should still default indexing to EIS endpoint - - match: { test_index_search.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } - # But use explicit search endpoint - - match: { test_index_search.mappings.properties.my_semantic_field.search_inference_id: "test-elser-service" } - ---- -"Test semantic_text field mapping creation and document indexing": - - - do: - indices.create: - index: test_index_docs - body: - mappings: - properties: - my_semantic_field: - type: semantic_text - settings: - number_of_shards: 1 - number_of_replicas: 0 - - # Index a document - - do: - index: - index: test_index_docs - id: doc1 - body: - my_semantic_field: "Hello world semantic search" - - - do: - indices.refresh: - index: test_index_docs - - # Verify document was indexed - - do: - get: - index: test_index_docs - id: doc1 - - - match: { _source.my_semantic_field: "Hello world semantic search" } - - # Verify the mapping was created correctly - - do: - indices.get_mapping: - index: test_index_docs - - - match: { test_index_docs.mappings.properties.my_semantic_field.type: "semantic_text" } - - match: { test_index_docs.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } \ No newline at end of file diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml deleted file mode 100644 index 3f8cc6f6e218c..0000000000000 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_eis_default.yml +++ /dev/null @@ -1,29 +0,0 @@ -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License -# 2.0; you may not use this file except in compliance with the Elastic License -# 2.0. -# ---- -"Test semantic_text defaults to EIS when available": - - skip: - features: [ test_plugin ] - - - do: - indices.create: - index: test_semantic_text_eis_default - body: - mappings: - properties: - my_semantic_field: - type: "semantic_text" - - - do: - indices.get_mapping: - index: test_semantic_text_eis_default - - - match: { test_semantic_text_eis_default.mappings.properties.my_semantic_field.inference_id: ".elser-2-elastic" } - - - do: - indices.delete: - index: test_semantic_text_eis_default diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml deleted file mode 100644 index 4501cf11ef2c5..0000000000000 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/31_semantic_text_ml_fallback.yml +++ /dev/null @@ -1,27 +0,0 @@ -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License -# 2.0; you may not use this file except in compliance with the Elastic License -# 2.0. -# ---- -"Test semantic_text falls back to ML node model when EIS is not available": - - - do: - indices.create: - index: test_semantic_text_ml_fallback - body: - mappings: - properties: - my_semantic_field: - type: "semantic_text" - - - do: - indices.get_mapping: - index: test_semantic_text_ml_fallback - - - match: { test_semantic_text_ml_fallback.mappings.properties.my_semantic_field.inference_id: ".elser-2-elasticsearch" } - - - do: - indices.delete: - index: test_semantic_text_ml_fallback From b4ed763794029d5e236b6462a705852154604f7d Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 3 Oct 2025 01:09:40 +0100 Subject: [PATCH 13/28] Unit tests and mock is working --- .../mapper/SemanticTextFieldMapper.java | 2 +- .../mapper/SemanticTextFieldMapperTests.java | 47 ++++--------------- 2 files changed, 11 insertions(+), 38 deletions(-) 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 30d838c5fd7ec..27fc6c21a2c75 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 @@ -153,7 +153,7 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie public static final String CONTENT_TYPE = "semantic_text"; public static final String DEFAULT_FALLBACK_ELSER_INFERENCE_ID = DEFAULT_ELSER_ID; - private static final String DEFAULT_EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2; + public static final String DEFAULT_EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2; public static final float DEFAULT_RESCORE_OVERSAMPLE = 3.0f; 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 6b1c510265c5e..b2905a4bed6f6 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 @@ -18,7 +18,6 @@ import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; @@ -112,13 +111,13 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getChunksFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_RESCORE_OVERSAMPLE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.INDEX_OPTIONS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.UNSUPPORTED_INDEX_MESSAGE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettings; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.generateRandomChunkingSettingsOtherThan; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldTests.randomSemanticText; -import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -176,7 +175,7 @@ protected Supplier getModelRegistry() { private void registerDefaultEisEndpoint() { globalModelRegistry.putDefaultIdIfAbsent( new InferenceService.DefaultConfigId( - DEFAULT_ELSER_ENDPOINT_ID_V2, + DEFAULT_EIS_ELSER_INFERENCE_ID, MinimalServiceSettings.sparseEmbedding(ElasticInferenceService.NAME), mock(InferenceService.class) ) @@ -237,7 +236,7 @@ protected void minimalMapping(XContentBuilder b) throws IOException { @Override protected void metaMapping(XContentBuilder b) throws IOException { super.metaMapping(b); - b.field(INFERENCE_ID_FIELD, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); + b.field(INFERENCE_ID_FIELD, DEFAULT_EIS_ELSER_INFERENCE_ID); } @Override @@ -305,7 +304,7 @@ public void testDefaults() throws Exception { DocumentMapper mapper = mapperService.documentMapper(); assertEquals(Strings.toString(expectedMapping), mapper.mappingSource().toString()); assertSemanticTextField(mapperService, fieldName, false, null, null); - assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID); ParsedDocument doc1 = mapper.parse(source(this::writeField)); List fields = doc1.rootDoc().getFields("field"); @@ -319,9 +318,9 @@ public void testDefaultInferenceIdUsesEisWhenAvailable() throws Exception { final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_ENDPOINT_ID_V2, DEFAULT_ELSER_ENDPOINT_ID_V2); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID); DocumentMapper mapper = mapperService.documentMapper(); - assertThat(mapper.mappingSource().toString(), containsString("\"inference_id\":\"" + DEFAULT_ELSER_ENDPOINT_ID_V2 + "\"")); + assertThat(mapper.mappingSource().toString(), containsString("\"inference_id\":\"" + DEFAULT_EIS_ELSER_INFERENCE_ID + "\"")); } public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception { @@ -347,31 +346,6 @@ public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception } } - public void testDynamicElserDefaultSelection() throws Exception { - final String fieldName = "field"; - final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); - - // Test 1: When EIS is available, should default to .elser-2-elastic - when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenReturn(true); - MapperService mapperServiceWithEis = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperServiceWithEis, fieldName, DEFAULT_ELSER_ENDPOINT_ID_V2, DEFAULT_ELSER_ENDPOINT_ID_V2); - - // Test 2: When EIS is not available, should fallback to .elser-2-elasticsearch - when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenReturn(false); - MapperService mapperServiceWithoutEis = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperServiceWithoutEis, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); - } - - public void testDynamicElserDefaultSelectionEdgeCases() throws Exception { - final String fieldName = "field"; - final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); - - // Test: ModelRegistry throws exception - should fallback gracefully - when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenThrow(new RuntimeException("Registry error")); - MapperService mapperServiceWithError = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperServiceWithError, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); - } - public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception { final String fieldName = "field"; final String explicitInferenceId = "my-custom-model"; @@ -380,7 +354,6 @@ public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception ); // Even when EIS is available, explicit inference_id should take precedence - when(globalModelRegistry.containsDefaultConfigId(DEFAULT_ELSER_ENDPOINT_ID_V2)).thenReturn(true); MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); assertInferenceEndpoints(mapperService, fieldName, explicitInferenceId, explicitInferenceId); } @@ -415,12 +388,12 @@ public void testSetInferenceEndpoints() throws IOException { ); final XContentBuilder expectedMapping = fieldMapping( b -> b.field("type", "semantic_text") - .field(INFERENCE_ID_FIELD, DEFAULT_ELSER_2_INFERENCE_ID) + .field(INFERENCE_ID_FIELD, DEFAULT_EIS_ELSER_INFERENCE_ID) .field(SEARCH_INFERENCE_ID_FIELD, searchInferenceId) ); final MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); assertSemanticTextField(mapperService, fieldName, false, null, null); - assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, searchInferenceId); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, searchInferenceId); assertSerialization.accept(expectedMapping, mapperService); } { @@ -1841,8 +1814,8 @@ public static SemanticTextIndexOptions randomSemanticTextIndexOptions(TaskType t @Override protected void assertExistsQuery(MappedFieldType fieldType, Query query, LuceneDocument fields) { - // Until a doc is indexed, the query is rewritten as match no docs - assertThat(query, instanceOf(MatchNoDocsQuery.class)); + // With an inference defaults creating nested chunk documents, expect a nested query + assertThat(query, instanceOf(ESToParentBlockJoinQuery.class)); } private static void addSemanticTextMapping( From 4f0d7c066191c6dd12e4cbc5c86190432e73cdeb Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 3 Oct 2025 00:15:45 +0000 Subject: [PATCH 14/28] [CI] Auto commit changes from spotless --- .../mapper/SemanticTextFieldMapperTests.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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 b2905a4bed6f6..b831853d4bc01 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 @@ -110,8 +110,8 @@ import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.TEXT_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getChunksFieldName; import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName; -import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_RESCORE_OVERSAMPLE; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.INDEX_OPTIONS_FIELD; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.UNSUPPORTED_INDEX_MESSAGE; @@ -122,8 +122,8 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public class SemanticTextFieldMapperTests extends MapperTestCase { @@ -330,12 +330,7 @@ public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception globalModelRegistry.clearDefaultIds(); try { MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints( - mapperService, - fieldName, - DEFAULT_FALLBACK_ELSER_INFERENCE_ID, - DEFAULT_FALLBACK_ELSER_INFERENCE_ID - ); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); DocumentMapper mapper = mapperService.documentMapper(); assertThat( mapper.mappingSource().toString(), From 18f38b8456542e71d478b93df35857a072a5053b Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 3 Oct 2025 01:40:03 +0100 Subject: [PATCH 15/28] Fix test --- .../xpack/inference/mapper/SemanticTextFieldMapperTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ece820b2dbf56..8dc7a1d3e8b3c 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 @@ -388,7 +388,7 @@ public void testSetInferenceEndpoints() throws IOException { ); final MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); assertSemanticTextField(mapperService, fieldName, false, null, null); - assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, searchInferenceId); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, searchInferenceId); assertSerialization.accept(expectedMapping, mapperService); } { From cc2e978d499e4b1138220f14bab5e81e6cc60894 Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 3 Oct 2025 02:58:59 +0100 Subject: [PATCH 16/28] yaml addition failure --- .../TestDefaultInferenceServiceExtension.java | 81 +++++++++++++++++++ ...search.inference.InferenceServiceExtension | 1 + .../10_semantic_text_field_mapping.yml | 66 ++++++++++++++- 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java new file mode 100644 index 0000000000000..0660bdf50d66c --- /dev/null +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference.mock; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.inference.InferenceService; +import org.elasticsearch.inference.InferenceServiceExtension; +import org.elasticsearch.inference.MinimalServiceSettings; +import org.elasticsearch.inference.Model; +import org.elasticsearch.inference.TaskType; +import java.util.List; + +public class TestDefaultInferenceServiceExtension implements InferenceServiceExtension { + + @Override + public List getInferenceServiceFactories() { + return List.of(TestDefaultInferenceService::new); + } + + public static class TestDefaultInferenceService extends TestSparseInferenceServiceExtension.TestInferenceService { + + private static final String DEFAULT_EIS_ELSER_INFERENCE_ID = ".elser-2-elastic"; + private static final String DEFAULT_FALLBACK_ELSER_INFERENCE_ID = ".elastic-inference"; + + public static final String NAME = "default_inference_test_service"; + + public TestDefaultInferenceService(InferenceServiceFactoryContext context) { + super(context); + } + + @Override + public String name() { + return NAME; + } + + @Override + public List defaultConfigIds() { + return List.of( + new InferenceService.DefaultConfigId( + DEFAULT_EIS_ELSER_INFERENCE_ID, + MinimalServiceSettings.sparseEmbedding(name()), + this + ), + new InferenceService.DefaultConfigId( + DEFAULT_FALLBACK_ELSER_INFERENCE_ID, + MinimalServiceSettings.sparseEmbedding(name()), + this + ) + ); + } + + @Override + public void defaultConfigs(ActionListener> defaultsListener) { + defaultsListener.onResponse( + List.of( + new TestServiceModel( + DEFAULT_EIS_ELSER_INFERENCE_ID, + TaskType.SPARSE_EMBEDDING, + name(), + new TestSparseInferenceServiceExtension.TestServiceSettings("default_eis_model", null, false), + new TestTaskSettings((Integer) null), + new TestSecretSettings("default_eis_api_key") + ), + new TestServiceModel( + DEFAULT_FALLBACK_ELSER_INFERENCE_ID, + TaskType.SPARSE_EMBEDDING, + name(), + new TestSparseInferenceServiceExtension.TestServiceSettings("default_fallback_model", null, false), + new TestTaskSettings((Integer) null), + new TestSecretSettings("default_fallback_api_key") + ) + ) + ); + } + } +} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension index a481e2a4a0451..dc068782c67ab 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension @@ -3,3 +3,4 @@ org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension org.elasticsearch.xpack.inference.mock.TestRerankingServiceExtension org.elasticsearch.xpack.inference.mock.TestStreamingCompletionServiceExtension org.elasticsearch.xpack.inference.mock.TestCompletionServiceExtension +org.elasticsearch.xpack.inference.mock.TestDefaultInferenceServiceExtension 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..2333f52f6f9d4 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 @@ -460,6 +460,9 @@ setup: path: /_inference capabilities: [ default_elser_2 ] + - do: + inference.get: {} + - do: indices.create: index: test-always-include-inference-id-index @@ -473,7 +476,68 @@ setup: indices.get_mapping: index: test-always-include-inference-id-index - - exists: test-always-include-inference-id-index.mappings.properties.semantic_field.inference_id + - match: { test-always-include-inference-id-index.mappings.properties.semantic_field.inference_id: ".elser-2-elastic" } + - match: { test-always-include-inference-id-index.mappings.properties.semantic_field.search_inference_id: ".elser-2-elastic" } + +--- +"Mapping falls back to elastic inference when EIS default removed": + - requires: + cluster_features: "semantic_text.always_emit_inference_id_fix" + reason: always emit inference ID fix added in 8.17.0 + test_runner_features: [ capabilities ] + capabilities: + - method: GET + path: /_inference + capabilities: [ default_elser_2 ] + + - do: + inference.get: {} + + - do: + capabilities: { } + + - do: + inference.delete: + task_type: sparse_embedding + inference_id: ".elser-2-elastic" + force: true + + - do: + indices.create: + index: test-inference-default-fallback + body: + mappings: + properties: + semantic_field: + type: semantic_text + + - do: + indices.get_mapping: + index: test-inference-default-fallback + + - match: { test-inference-default-fallback.mappings.properties.semantic_field.inference_id: ".elastic-inference" } + - match: { test-inference-default-fallback.mappings.properties.semantic_field.search_inference_id: ".elastic-inference" } + + # Restore the preferred default inference endpoint for subsequent tests. + - do: + inference.put: + task_type: sparse_embedding + inference_id: ".elser-2-elastic" + body: > + { + "service": "default_inference_test_service", + "service_settings": { + "model": "default_eis_model", + "api_key": "default_eis_api_key" + }, + "task_settings": { + } + } + + - do: + indices.delete: + index: test-inference-default-fallback + ignore_unavailable: true --- "Field caps exclude chunks and embedding fields": From 33f4eaac668923e281e39be1c10fcfc898ca7eee Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 3 Oct 2025 02:06:08 +0000 Subject: [PATCH 17/28] [CI] Auto commit changes from spotless --- .../mock/TestDefaultInferenceServiceExtension.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java index 0660bdf50d66c..589ee66bcceb7 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java @@ -13,6 +13,7 @@ import org.elasticsearch.inference.MinimalServiceSettings; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.TaskType; + import java.util.List; public class TestDefaultInferenceServiceExtension implements InferenceServiceExtension { @@ -41,11 +42,7 @@ public String name() { @Override public List defaultConfigIds() { return List.of( - new InferenceService.DefaultConfigId( - DEFAULT_EIS_ELSER_INFERENCE_ID, - MinimalServiceSettings.sparseEmbedding(name()), - this - ), + new InferenceService.DefaultConfigId(DEFAULT_EIS_ELSER_INFERENCE_ID, MinimalServiceSettings.sparseEmbedding(name()), this), new InferenceService.DefaultConfigId( DEFAULT_FALLBACK_ELSER_INFERENCE_ID, MinimalServiceSettings.sparseEmbedding(name()), From 6ef75435225c4b72f8a8c26003d974e26c088e70 Mon Sep 17 00:00:00 2001 From: Mridula Date: Mon, 6 Oct 2025 10:36:48 +0100 Subject: [PATCH 18/28] Resolved the import issue or duplication of variables in mock --- .../mock/TestDefaultInferenceServiceExtension.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java index 589ee66bcceb7..39cc7eb5eb464 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java @@ -7,6 +7,9 @@ package org.elasticsearch.xpack.inference.mock; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID; + import org.elasticsearch.action.ActionListener; import org.elasticsearch.inference.InferenceService; import org.elasticsearch.inference.InferenceServiceExtension; @@ -25,9 +28,6 @@ public List getInferenceServiceFactories() { public static class TestDefaultInferenceService extends TestSparseInferenceServiceExtension.TestInferenceService { - private static final String DEFAULT_EIS_ELSER_INFERENCE_ID = ".elser-2-elastic"; - private static final String DEFAULT_FALLBACK_ELSER_INFERENCE_ID = ".elastic-inference"; - public static final String NAME = "default_inference_test_service"; public TestDefaultInferenceService(InferenceServiceFactoryContext context) { From 7733cfe42b51cc1dafba1cb22a059bd41b1b0155 Mon Sep 17 00:00:00 2001 From: Mridula Date: Tue, 7 Oct 2025 13:10:37 +0100 Subject: [PATCH 19/28] Resolved PR comments --- .../TestDefaultInferenceServiceExtension.java | 78 - ...search.inference.InferenceServiceExtension | 3 +- .../mapper/SemanticTextFieldMapper.java | 4 + .../mapper/SemanticTextFieldMapperTests.java | 40 +- .../10_semantic_text_field_mapping.yml | 1545 ----------------- 5 files changed, 20 insertions(+), 1650 deletions(-) delete mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java delete mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java deleted file mode 100644 index 39cc7eb5eb464..0000000000000 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDefaultInferenceServiceExtension.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference.mock; - -import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID; -import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.inference.InferenceService; -import org.elasticsearch.inference.InferenceServiceExtension; -import org.elasticsearch.inference.MinimalServiceSettings; -import org.elasticsearch.inference.Model; -import org.elasticsearch.inference.TaskType; - -import java.util.List; - -public class TestDefaultInferenceServiceExtension implements InferenceServiceExtension { - - @Override - public List getInferenceServiceFactories() { - return List.of(TestDefaultInferenceService::new); - } - - public static class TestDefaultInferenceService extends TestSparseInferenceServiceExtension.TestInferenceService { - - public static final String NAME = "default_inference_test_service"; - - public TestDefaultInferenceService(InferenceServiceFactoryContext context) { - super(context); - } - - @Override - public String name() { - return NAME; - } - - @Override - public List defaultConfigIds() { - return List.of( - new InferenceService.DefaultConfigId(DEFAULT_EIS_ELSER_INFERENCE_ID, MinimalServiceSettings.sparseEmbedding(name()), this), - new InferenceService.DefaultConfigId( - DEFAULT_FALLBACK_ELSER_INFERENCE_ID, - MinimalServiceSettings.sparseEmbedding(name()), - this - ) - ); - } - - @Override - public void defaultConfigs(ActionListener> defaultsListener) { - defaultsListener.onResponse( - List.of( - new TestServiceModel( - DEFAULT_EIS_ELSER_INFERENCE_ID, - TaskType.SPARSE_EMBEDDING, - name(), - new TestSparseInferenceServiceExtension.TestServiceSettings("default_eis_model", null, false), - new TestTaskSettings((Integer) null), - new TestSecretSettings("default_eis_api_key") - ), - new TestServiceModel( - DEFAULT_FALLBACK_ELSER_INFERENCE_ID, - TaskType.SPARSE_EMBEDDING, - name(), - new TestSparseInferenceServiceExtension.TestServiceSettings("default_fallback_model", null, false), - new TestTaskSettings((Integer) null), - new TestSecretSettings("default_fallback_api_key") - ) - ) - ); - } - } -} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension index dc068782c67ab..4de94117c902b 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension @@ -2,5 +2,4 @@ org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension org.elasticsearch.xpack.inference.mock.TestRerankingServiceExtension org.elasticsearch.xpack.inference.mock.TestStreamingCompletionServiceExtension -org.elasticsearch.xpack.inference.mock.TestCompletionServiceExtension -org.elasticsearch.xpack.inference.mock.TestDefaultInferenceServiceExtension +org.elasticsearch.xpack.inference.mock.TestCompletionServiceExtension \ No newline at end of file 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 b3c5e2990639b..d30c049b39e1e 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 @@ -889,6 +889,10 @@ public Query termQuery(Object value, SearchExecutionContext context) { @Override public Query existsQuery(SearchExecutionContext context) { + // If this field has never seen inference results (no model settings), there are no values yet + if (modelSettings == null) { + return new MatchNoDocsQuery(); + } if (getEmbeddingsField() == null) { return new MatchNoDocsQuery(); } 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 8dc7a1d3e8b3c..da5d8b367f250 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 @@ -18,6 +18,7 @@ import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; @@ -25,6 +26,7 @@ import org.apache.lucene.search.join.QueryBitSetProducer; import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.CheckedBiConsumer; @@ -138,7 +140,7 @@ public SemanticTextFieldMapperTests(boolean useLegacyFormat) { ModelRegistry globalModelRegistry; @Before - private void startThreadPool() { + private void initializeTestEnvironment() { threadPool = createThreadPool(); var clusterService = ClusterServiceUtils.createClusterService(threadPool); var modelRegistry = new ModelRegistry(clusterService, new NoOpClient(threadPool)); @@ -327,30 +329,18 @@ public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception final String fieldName = "field"; final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping); - globalModelRegistry.clearDefaultIds(); - try { - MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); - DocumentMapper mapper = mapperService.documentMapper(); - assertThat( - mapper.mappingSource().toString(), - containsString("\"inference_id\":\"" + DEFAULT_FALLBACK_ELSER_INFERENCE_ID + "\"") - ); - } finally { - registerDefaultEisEndpoint(); - } - } + removeDefaultEisEndpoint(); - public void testExplicitInferenceIdOverridesDynamicSelection() throws Exception { - final String fieldName = "field"; - final String explicitInferenceId = "my-custom-model"; - final XContentBuilder fieldMapping = fieldMapping( - b -> b.field("type", "semantic_text").field(INFERENCE_ID_FIELD, explicitInferenceId) - ); - - // Even when EIS is available, explicit inference_id should take precedence MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); - assertInferenceEndpoints(mapperService, fieldName, explicitInferenceId, explicitInferenceId); + assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); + DocumentMapper mapper = mapperService.documentMapper(); + assertThat(mapper.mappingSource().toString(), containsString("\"inference_id\":\"" + DEFAULT_FALLBACK_ELSER_INFERENCE_ID + "\"")); + } + + private void removeDefaultEisEndpoint() { + PlainActionFuture removalFuture = new PlainActionFuture<>(); + globalModelRegistry.removeDefaultConfigs(Set.of(DEFAULT_EIS_ELSER_INFERENCE_ID), removalFuture); + assertTrue("Failed to remove default EIS endpoint", removalFuture.actionGet()); } @Override @@ -1851,8 +1841,8 @@ public static SemanticTextIndexOptions randomSemanticTextIndexOptions(TaskType t @Override protected void assertExistsQuery(MappedFieldType fieldType, Query query, LuceneDocument fields) { - // With an inference defaults creating nested chunk documents, expect a nested query - assertThat(query, instanceOf(ESToParentBlockJoinQuery.class)); + // Until a doc is indexed, the query is rewritten as match no docs + assertThat(query, instanceOf(MatchNoDocsQuery.class)); } private static void addSemanticTextMapping( 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 deleted file mode 100644 index 2333f52f6f9d4..0000000000000 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml +++ /dev/null @@ -1,1545 +0,0 @@ -setup: - - requires: - cluster_features: "gte_v8.15.0" - reason: semantic_text introduced in 8.15.0 - - - do: - inference.put: - task_type: sparse_embedding - inference_id: sparse-inference-id - body: > - { - "service": "test_service", - "service_settings": { - "model": "my_model", - "api_key": "abc64" - }, - "task_settings": { - } - } - - - do: - inference.put: - task_type: text_embedding - inference_id: dense-inference-id - body: > - { - "service": "text_embedding_test_service", - "service_settings": { - "model": "my_model", - "dimensions": 4, - "similarity": "cosine", - "api_key": "abc64" - }, - "task_settings": { - } - } - - - do: - inference.put: - task_type: text_embedding - inference_id: dense-inference-id-compatible-with-bbq - body: > - { - "service": "text_embedding_test_service", - "service_settings": { - "model": "my_model", - "dimensions": 64, - "similarity": "cosine", - "api_key": "abc64" - }, - "task_settings": { - } - } - - - - do: - indices.create: - index: test-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - sparse_field: - type: semantic_text - inference_id: sparse-inference-id - dense_field: - type: semantic_text - inference_id: dense-inference-id - ---- -"Indexes sparse vector document": - # Checks mapping is not updated until first doc arrives - - 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 } - - length: { "test-index.mappings.properties.sparse_field": 2 } - - - do: - index: - index: test-index - id: doc_1 - body: - sparse_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.sparse_field: - inference: - inference_id: sparse-inference-id - model_settings: - task_type: sparse_embedding - chunks: - sparse_field: - - start_offset: 0 - end_offset: 44 - embeddings: - feature_0: 1.0 - feature_1: 2.0 - feature_2: 3.0 - feature_3: 4.0 - - start_offset: 44 - end_offset: 67 - embeddings: - feature_4: 0.1 - feature_5: 0.2 - feature_6: 0.3 - feature_7: 0.4 - - # Checks mapping is updated when first doc arrives - - 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.task_type": sparse_embedding } - - length: { "test-index.mappings.properties.sparse_field": 3 } - ---- -"Field caps with sparse embedding": - - requires: - cluster_features: "gte_v8.16.0" - reason: field_caps support for semantic_text added in 8.16.0 - - - do: - field_caps: - include_empty_fields: true - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - exists: fields.sparse_field - - exists: fields.dense_field - - - do: - field_caps: - include_empty_fields: false - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - not_exists: fields.sparse_field - - not_exists: fields.dense_field - - - do: - index: - index: test-index - id: doc_1 - body: - sparse_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.sparse_field: - inference: - inference_id: sparse-inference-id - model_settings: - task_type: sparse_embedding - chunks: - sparse_field: - - start_offset: 0 - end_offset: 44 - embeddings: - feature_0: 1.0 - feature_1: 2.0 - feature_2: 3.0 - feature_3: 4.0 - - start_offset: 44 - end_offset: 67 - embeddings: - feature_4: 0.1 - feature_5: 0.2 - feature_6: 0.3 - feature_7: 0.4 - refresh: true - - - do: - field_caps: - include_empty_fields: true - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - exists: fields.sparse_field - - exists: fields.dense_field - - match: { fields.sparse_field.text.searchable: true } - - match: { fields.dense_field.text.searchable: true } - - - do: - field_caps: - include_empty_fields: false - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - exists: fields.sparse_field - - not_exists: fields.dense_field - - match: { fields.sparse_field.text.searchable: true } - ---- -"Indexes dense vector document": - # Checks mapping is not updated until first doc arrives - - 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 } - - not_exists: test-index.mappings.properties.dense_field.model_settings - - - do: - index: - index: test-index - id: doc_2 - body: - dense_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.dense_field: - inference: - inference_id: dense-inference-id - model_settings: - task_type: text_embedding - dimensions: 4 - similarity: cosine - element_type: float - chunks: - dense_field: - - start_offset: 0 - end_offset: 44 - embeddings: [ 0.04673296958208084, -0.03237321600317955, -0.02543032355606556, 0.056035321205854416 ] - - start_offset: 44 - end_offset: 67 - embeddings: [ 0.00641461368650198, -0.0016253676731139421, -0.05126338079571724, 0.053438711911439896 ] - - # Checks mapping is updated when first doc arrives - - 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.task_type": text_embedding } - - exists: test-index.mappings.properties.dense_field.model_settings - ---- -"Indexes dense vector document with bbq compatible model": - - requires: - cluster_features: "semantic_text.index_options" - reason: index_options introduced in 8.19.0 - - - do: - indices.create: - index: test-index-options-with-bbq - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - dense_field: - type: semantic_text - inference_id: dense-inference-id-compatible-with-bbq - - # Checks vector mapping is not updated until first doc arrives - - do: - indices.get_mapping: - index: test-index-options-with-bbq - - - match: { "test-index-options-with-bbq.mappings.properties.dense_field.type": semantic_text } - - match: { "test-index-options-with-bbq.mappings.properties.dense_field.inference_id": dense-inference-id-compatible-with-bbq } - - not_exists: test-index-options-with-bbq.mappings.properties.dense_field.index_options - - not_exists: test-index-options-with-bbq.mappings.properties.dense_field.model_settings - - - do: - index: - index: test-index-options-with-bbq - id: doc_2 - body: - dense_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.dense_field: - inference: - inference_id: dense-inference-id-compatible-with-bbq - model_settings: - task_type: text_embedding - dimensions: 64 - similarity: cosine - element_type: float - chunks: - dense_field: - - start_offset: 0 - end_offset: 44 - embeddings: [ 0.05, -0.03, -0.03, 0.06, 0.01, -0.02, 0.07, 0.02, -0.04, 0.03, 0.00, 0.05, -0.06, 0.04, -0.01, 0.02, -0.05, 0.01, 0.03, -0.02, 0.06, -0.04, 0.00, 0.05, -0.03, 0.02, 0.01, -0.01, 0.04, -0.06, 0.03, 0.02, -0.02, 0.06, -0.01, 0.00, 0.04, -0.05, 0.01, 0.03, -0.04, 0.02, -0.03, 0.05, -0.02, 0.01, 0.03, -0.06, 0.04, 0.00, -0.01, 0.06, -0.03, 0.02, 0.01, -0.04, 0.05, -0.01, 0.00, 0.04, -0.05, 0.02, 0.03, -0.02 ] - - start_offset: 44 - end_offset: 67 - embeddings: [ 0.05, -0.03, -0.03, 0.06, 0.01, -0.02, 0.07, 0.02, -0.04, 0.03, 0.00, 0.05, -0.06, 0.04, -0.01, 0.02, -0.05, 0.01, 0.03, -0.02, 0.06, -0.04, 0.00, 0.05, -0.03, 0.02, 0.01, -0.01, 0.04, -0.06, 0.03, 0.02, -0.02, 0.06, -0.01, 0.00, 0.04, -0.05, 0.01, 0.03, -0.04, 0.02, -0.03, 0.05, -0.02, 0.01, 0.03, -0.06, 0.04, 0.00, -0.01, 0.06, -0.03, 0.02, 0.01, -0.04, 0.05, -0.01, 0.00, 0.04, -0.05, 0.02, 0.03, -0.02 ] - - - # Checks mapping is updated when first doc arrives - - do: - indices.get_mapping: - index: test-index-options-with-bbq - - - match: { "test-index-options-with-bbq.mappings.properties.dense_field.type": semantic_text } - - match: { "test-index-options-with-bbq.mappings.properties.dense_field.inference_id": dense-inference-id-compatible-with-bbq } - - match: { "test-index-options-with-bbq.mappings.properties.dense_field.model_settings.task_type": text_embedding } - - not_exists: test-index-options-with-bbq.mappings.properties.dense_field.index_options - ---- -"Field caps with text embedding": - - requires: - cluster_features: "gte_v8.16.0" - reason: field_caps support for semantic_text added in 8.16.0 - - - do: - field_caps: - include_empty_fields: true - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - exists: fields.sparse_field - - exists: fields.dense_field - - - do: - field_caps: - include_empty_fields: false - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - not_exists: fields.sparse_field - - not_exists: fields.dense_field - - - do: - index: - index: test-index - id: doc_2 - body: - dense_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.dense_field: - inference: - inference_id: dense-inference-id - model_settings: - task_type: text_embedding - dimensions: 4 - similarity: cosine - element_type: float - chunks: - dense_field: - - start_offset: 0 - end_offset: 44 - embeddings: [ 0.04673296958208084, -0.03237321600317955, -0.02543032355606556, 0.056035321205854416 ] - - start_offset: 44 - end_offset: 67 - embeddings: [ 0.00641461368650198, -0.0016253676731139421, -0.05126338079571724, 0.053438711911439896 ] - refresh: true - - - do: - field_caps: - include_empty_fields: true - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - exists: fields.sparse_field - - exists: fields.dense_field - - match: { fields.sparse_field.text.searchable: true } - - match: { fields.dense_field.text.searchable: true } - - - do: - field_caps: - include_empty_fields: false - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - not_exists: fields.sparse_field - - exists: fields.dense_field - - match: { fields.dense_field.text.searchable: true } - ---- -"Cannot be used directly as a nested field": - - - do: - catch: /semantic_text field \[nested.semantic\] cannot be nested/ - indices.create: - index: test-nested-index - body: - mappings: - properties: - nested: - type: nested - properties: - semantic: - type: semantic_text - inference_id: sparse-inference-id - another_field: - type: keyword - ---- -"Cannot be used as a nested field on nested objects": - - - do: - catch: /semantic_text field \[nested.nested_object.semantic\] cannot be nested/ - indices.create: - index: test-nested-index - body: - mappings: - properties: - nested: - type: nested - properties: - nested_object: - type: object - properties: - semantic: - type: semantic_text - inference_id: sparse-inference-id - another_field: - type: keyword - ---- -"Cannot be in an object field with subobjects disabled": - - requires: - cluster_features: "semantic_text.in_object_field_fix" - reason: object field fix added in 8.16.0 & 8.15.4 - - - do: - catch: bad_request - indices.create: - index: test-subobjects-index - body: - mappings: - properties: - level_1: - type: object - properties: - level_2: - type: object - subobjects: false - properties: - sparse_field: - type: semantic_text - inference_id: sparse-inference-id - - - match: { error.type: illegal_argument_exception } - - match: { error.reason: "semantic_text field [level_1.level_2.sparse_field] cannot be in an object field with - subobjects disabled" } - ---- -"Mapping always includes inference ID": - - requires: - cluster_features: "semantic_text.always_emit_inference_id_fix" - reason: always emit inference ID fix added in 8.17.0 - test_runner_features: [ capabilities ] - capabilities: - - method: GET - path: /_inference - capabilities: [ default_elser_2 ] - - - do: - inference.get: {} - - - do: - indices.create: - index: test-always-include-inference-id-index - body: - mappings: - properties: - semantic_field: - type: semantic_text - - - do: - indices.get_mapping: - index: test-always-include-inference-id-index - - - match: { test-always-include-inference-id-index.mappings.properties.semantic_field.inference_id: ".elser-2-elastic" } - - match: { test-always-include-inference-id-index.mappings.properties.semantic_field.search_inference_id: ".elser-2-elastic" } - ---- -"Mapping falls back to elastic inference when EIS default removed": - - requires: - cluster_features: "semantic_text.always_emit_inference_id_fix" - reason: always emit inference ID fix added in 8.17.0 - test_runner_features: [ capabilities ] - capabilities: - - method: GET - path: /_inference - capabilities: [ default_elser_2 ] - - - do: - inference.get: {} - - - do: - capabilities: { } - - - do: - inference.delete: - task_type: sparse_embedding - inference_id: ".elser-2-elastic" - force: true - - - do: - indices.create: - index: test-inference-default-fallback - body: - mappings: - properties: - semantic_field: - type: semantic_text - - - do: - indices.get_mapping: - index: test-inference-default-fallback - - - match: { test-inference-default-fallback.mappings.properties.semantic_field.inference_id: ".elastic-inference" } - - match: { test-inference-default-fallback.mappings.properties.semantic_field.search_inference_id: ".elastic-inference" } - - # Restore the preferred default inference endpoint for subsequent tests. - - do: - inference.put: - task_type: sparse_embedding - inference_id: ".elser-2-elastic" - body: > - { - "service": "default_inference_test_service", - "service_settings": { - "model": "default_eis_model", - "api_key": "default_eis_api_key" - }, - "task_settings": { - } - } - - - do: - indices.delete: - index: test-inference-default-fallback - ignore_unavailable: true - ---- -"Field caps exclude chunks and embedding fields": - - requires: - cluster_features: "semantic_text.exclude_sub_fields_from_field_caps" - reason: field caps api exclude semantic_text subfields from 9.1.0 & 8.19.0 - - - do: - field_caps: - include_empty_fields: true - index: test-index - fields: "*" - - - match: { indices: [ "test-index" ] } - - exists: fields.sparse_field - - exists: fields.dense_field - - not_exists: fields.sparse_field.inference.chunks.embeddings - - not_exists: fields.sparse_field.inference.chunks.offset - - not_exists: fields.sparse_field.inference.chunks - - not_exists: fields.sparse_field.inference - - not_exists: fields.dense_field.inference.chunks.embeddings - - not_exists: fields.dense_field.inference.chunks.offset - - not_exists: fields.dense_field.inference.chunks - - not_exists: fields.dense_field.inference - ---- -"Field caps does not exclude multi-fields under semantic_text": - - requires: - cluster_features: "semantic_text.exclude_sub_fields_from_field_caps" - reason: field caps api exclude semantic_text subfields from 9.1.0 & 8.19.0 - - do: - indices.create: - index: test-multi-field-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - sparse_field: - type: semantic_text - inference_id: sparse-inference-id - fields: - sparse_keyword_field: - type: keyword - dense_field: - type: semantic_text - inference_id: dense-inference-id - fields: - dense_keyword_field: - type: keyword - - - do: - field_caps: - include_empty_fields: true - index: test-multi-field-index - fields: "*" - - - match: { indices: [ "test-multi-field-index" ] } - - exists: fields.sparse_field - - exists: fields.dense_field - - exists: fields.sparse_field\.sparse_keyword_field - - exists: fields.dense_field\.dense_keyword_field - - not_exists: fields.sparse_field.inference.chunks.embeddings - - not_exists: fields.sparse_field.inference.chunks.offset - - not_exists: fields.sparse_field.inference.chunks - - not_exists: fields.sparse_field.inference - - not_exists: fields.dense_field.inference.chunks.embeddings - - not_exists: fields.dense_field.inference.chunks.offset - - not_exists: fields.dense_field.inference.chunks - - not_exists: fields.dense_field.inference - ---- -"Field caps with semantic query does not fail": - - requires: - cluster_features: "semantic_query.filter_field_caps_fix" - reason: "fixed bug with semantic query filtering in field_caps (#116106)" - # We need at least one document present to exercise can-match phase - - do: - index: - index: test-index - id: doc_1 - body: - sparse_field: "This is a story about a cat and a dog." - refresh: true - - - do: - field_caps: - index: test-index - fields: "*" - body: - index_filter: - semantic: - field: "sparse_field" - query: "test" - - - match: { indices: [ "test-index" ] } - - match: { fields.sparse_field.text.searchable: true } - ---- -"Users can set dense vector index options and index documents using those options": - - requires: - cluster_features: "semantic_text.index_options" - reason: Index options introduced in 8.19.0 - - - do: - indices.create: - index: test-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - type: int8_hnsw - m: 20 - ef_construction: 100 - confidence_interval: 1.0 - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": "int8_hnsw" } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 20 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } - - - do: - index: - index: test-index-options - id: doc_1 - body: - semantic_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.semantic_field: - inference: - inference_id: dense-inference-id - model_settings: - task_type: text_embedding - dimensions: 4 - similarity: cosine - element_type: float - chunks: - semantic_field: - - start_offset: 0 - end_offset: 44 - embeddings: [ 0.04673296958208084, -0.03237321600317955, -0.02543032355606556, 0.056035321205854416 ] - - start_offset: 44 - end_offset: 67 - embeddings: [ 0.00641461368650198, -0.0016253676731139421, -0.05126338079571724, 0.053438711911439896 ] - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": int8_hnsw } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 20 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } - ---- -"Specifying incompatible dense vector index options will fail": - - requires: - cluster_features: "semantic_text.index_options" - reason: Index options introduced in 8.19.0 - - - do: - catch: /unsupported parameters/ - indices.create: - index: test-incompatible-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - type: bbq_flat - ef_construction: 100 - ---- -"Specifying unsupported index option types will fail": - - requires: - cluster_features: "semantic_text.index_options" - reason: Index options introduced in 8.19.0 - - - do: - catch: /Unsupported index options type/ - indices.create: - index: test-invalid-index-options-dense - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - type: foo - - do: - catch: bad_request - indices.create: - index: test-invalid-index-options-sparse - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - index_options: - sparse_vector: - type: int8_hnsw - ---- -"Index option type is required": - - requires: - cluster_features: "semantic_text.index_options" - reason: Index options introduced in 8.19.0 - - - do: - catch: /Required type/ - indices.create: - index: test-invalid-index-options-dense - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - foo: bar - ---- -"Specifying index options requires model information": - - requires: - cluster_features: "semantic_text.index_options" - reason: Index options introduced in 8.19.0 - - - do: - catch: /Model settings must be set to validate index options/ - indices.create: - index: my-custom-semantic-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: nonexistent-inference-id - index_options: - dense_vector: - type: int8_hnsw - - - match: { status: 400 } - - - do: - indices.create: - index: my-custom-semantic-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: nonexistent-inference-id - - - do: - indices.get_mapping: - index: my-custom-semantic-index - - - match: { "my-custom-semantic-index.mappings.properties.semantic_field.type": semantic_text } - - match: { "my-custom-semantic-index.mappings.properties.semantic_field.inference_id": nonexistent-inference-id } - - not_exists: my-custom-semantic-index.mappings.properties.semantic_field.index_options - ---- -"Updating index options": - - requires: - cluster_features: "semantic_text.index_options" - reason: Index options introduced in 8.19.0 - - - do: - indices.create: - index: test-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - type: int8_hnsw - m: 16 - ef_construction: 100 - confidence_interval: 1.0 - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": "int8_hnsw" } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 16 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } - - - do: - indices.put_mapping: - index: test-index-options - body: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - type: int8_hnsw - m: 20 - ef_construction: 90 - confidence_interval: 1.0 - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": "int8_hnsw" } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 20 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 90 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } - - - do: - catch: /Cannot update parameter \[index_options\]/ - indices.put_mapping: - index: test-index-options - body: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - dense_vector: - type: int8_flat - - - match: { status: 400 } - - ---- -"Displaying default index_options with and without include_defaults": - - requires: - cluster_features: "semantic_text.index_options_with_defaults" - reason: Index options defaults support introduced in 9.2.0 - - # Semantic text defaults to BBQ HNSW starting in 8.19.0/9.1.0 - - do: - indices.create: - index: test-index-options-dense - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id-compatible-with-bbq - - - do: - indices.get_mapping: - index: test-index-options-dense - - - not_exists: test-index-options-dense.mappings.properties.semantic_field.index_options - - - do: - indices.get_field_mapping: - index: test-index-options-dense - fields: semantic_field - include_defaults: true - - - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } - - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } - - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } - - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } - - # Validate that actually specifying the same values as our defaults will still serialize the user provided index_options - - do: - indices.create: - index: test-index-options-dense2 - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id-compatible-with-bbq - index_options: - dense_vector: - type: bbq_hnsw - m: 16 - ef_construction: 100 - rescore_vector: - oversample: 3 - - - do: - indices.get_mapping: - index: test-index-options-dense2 - - - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } - - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.m": 16 } - - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } - - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } - - - do: - indices.get_field_mapping: - index: test-index-options-dense2 - fields: semantic_field - include_defaults: true - - - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } - - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } - - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } - - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } - - # Indices not compatible with BBQ for whatever reason will fall back to whatever `dense_vector` defaults are. - - do: - indices.create: - index: test-index-options-dense-no-bbq - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - - - do: - indices.get_mapping: - index: test-index-options-dense-no-bbq - - - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options - - - do: - indices.get_field_mapping: - index: test-index-options-dense-no-bbq - fields: semantic_field - include_defaults: true - - - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options - - # Sparse embeddings models do not have index options for semantic_text in 8.19/9.1. - - do: - indices.create: - index: test-index-options-sparse - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - - - do: - indices.get_mapping: - index: test-index-options-sparse - - - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options - - - do: - indices.get_field_mapping: - index: test-index-options-sparse - fields: semantic_field - include_defaults: true - - - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options - ---- -"Users can set sparse vector index options and index documents using those options": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - indices.create: - index: test-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: true - pruning_config: - tokens_freq_ratio_threshold: 18.0 - tokens_weight_threshold: 0.6 - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 18.0 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.6 } - - - do: - index: - index: test-index-options - id: doc_1 - body: - semantic_field: "these are not the droids you're looking for. He's free to go around" - _inference_fields.semantic_field: - inference: - inference_id: sparse-inference-id - model_settings: - task_type: sparse_embedding - chunks: - semantic_field: - - start_offset: 0 - end_offset: 44 - embeddings: - dr: 1.6103356 - these: 1.1396849 - - start_offset: 44 - end_offset: 67 - embeddings: - free: 1.693662 - around: 1.4376559 - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 18.0 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.6 } - ---- -"Specifying invalid sparse vector index options will fail": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - catch: /\[index_options\] unknown field \[ef_construction\]/ - indices.create: - index: test-incompatible-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - ef_construction: 100 - - - match: { status: 400 } - - - do: - catch: /\[index_options\] field \[pruning_config\] should only be set if \[prune\] is set to true/ - indices.create: - index: test-incompatible-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: false - pruning_config: - tokens_freq_ratio_threshold: 18.0 - tokens_weight_threshold: 0.6 - - - match: { status: 400 } - - - do: - catch: /\[tokens_freq_ratio_threshold\] must be between \[1\] and \[100\]/ - indices.create: - index: test-incompatible-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: true - pruning_config: - tokens_freq_ratio_threshold: 101 - tokens_weight_threshold: 0.6 - - - match: { status: 400 } - - - do: - catch: /unknown field \[some_other_param\]/ - indices.create: - index: test-incompatible-index-options - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: true - pruning_config: - tokens_freq_ratio_threshold: 18.0 - tokens_weight_threshold: 0.6 - some_other_param: true - - - match: { status: 400 } - ---- -"Specifying sparse vector index options should fail using dense index options": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - catch: /Invalid task type for index options/ - indices.create: - index: my-custom-semantic-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - dense_vector: - type: bbq_hnsw - m: 16 - ef_construction: 100 - - - match: { status: 400 } - ---- -"Specifying dense vector index options should fail using sparse index options": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - catch: /Invalid task type for index options/ - indices.create: - index: my-custom-semantic-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: dense-inference-id - index_options: - sparse_vector: - prune: false - - - match: { status: 400 } - ---- -"Specifying sparse vector index options requires sparse vector model": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - catch: /Model settings must be set to validate index options/ - indices.create: - index: should-be-invalid-index - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: nonexistent-inference-id - index_options: - sparse_vector: - prune: false - - - match: { status: 400 } - ---- -"Updating sparse vector index options": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - indices.create: - index: test-index-options - body: - settings: - number_of_shards: 1 - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: true - pruning_config: - tokens_freq_ratio_threshold: 1.0 - tokens_weight_threshold: 1.0 - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 1.0 } - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 1.0 } - - - do: - index: - index: test-index-options - id: doc_1 - refresh: true - body: - semantic_field: "cheese is comet" - _inference_fields.semantic_field: - inference: - inference_id: sparse-inference-id - model_settings: - task_type: sparse_embedding - chunks: - semantic_field: - - start_offset: 0 - end_offset: 67 - embeddings: - feature_0: 2.671405 - feature_1: 0.11809908 - feature_2: 0.26088917 - - - do: - index: - index: test-index-options - id: doc_2 - refresh: true - body: - semantic_field: "planet is astronomy moon" - _inference_fields.semantic_field: - inference: - inference_id: sparse-inference-id - model_settings: - task_type: sparse_embedding - chunks: - semantic_field: - - start_offset: 0 - end_offset: 67 - embeddings: - feature_0: 2.3438394 - feature_1: 0.54600334 - feature_2: 0.36015007 - feature_3: 0.20022368 - - - do: - index: - index: test-index-options - id: doc_3 - refresh: true - body: - semantic_field: "is globe ocean underground" - _inference_fields.semantic_field: - inference: - inference_id: sparse-inference-id - model_settings: - task_type: sparse_embedding - chunks: - semantic_field: - - start_offset: 0 - end_offset: 67 - embeddings: - feature_0: 0.6891394 - feature_1: 0.484035 - feature_2: 0.080102935 - feature_3: 0.053516876 - - - do: - search: - index: test-index-options - body: - query: - semantic: - field: "semantic_field" - query: "test query" - - - match: { hits.total.value: 2 } - - match: { hits.hits.0._id: "doc_2" } - - match: { hits.hits.1._id: "doc_3" } - - - do: - indices.put_mapping: - index: test-index-options - body: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: false - - - do: - indices.get_mapping: - index: test-index-options - - - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": false } - - not_exists: "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold" - - not_exists: "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold" - - - do: - search: - index: test-index-options - body: - query: - semantic: - field: "semantic_field" - query: "test query" - - - match: { hits.total.value: 3 } - - match: { hits.hits.0._id: "doc_2" } - - match: { hits.hits.1._id: "doc_1" } - - match: { hits.hits.2._id: "doc_3" } - - ---- -"Displaying default sparse vector index_options with and without include_defaults": - - requires: - cluster_features: "semantic_text.sparse_vector_index_options" - reason: Index options for sparse vector introduced in 9.2.0 - - - do: - indices.create: - index: test-index-options-sparse - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - - - do: - indices.get_mapping: - index: test-index-options-sparse - - - not_exists: test-index-options-sparse.mappings.semantic_field.mapping.index_options - - - do: - indices.get_field_mapping: - index: test-index-options-sparse - fields: semantic_field - include_defaults: true - - - match: { "test-index-options-sparse.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.prune": true } - - match: { "test-index-options-sparse.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 5.0 } - - match: { "test-index-options-sparse.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.4 } - - # Validate that actually specifying the same values as our defaults will still serialize the user provided index_options - - do: - indices.create: - index: test-index-options-sparse2 - body: - settings: - index: - mapping: - semantic_text: - use_legacy_format: false - mappings: - properties: - semantic_field: - type: semantic_text - inference_id: sparse-inference-id - index_options: - sparse_vector: - prune: true - pruning_config: - tokens_freq_ratio_threshold: 5.0 - tokens_weight_threshold: 0.4 - - - do: - indices.get_mapping: - index: test-index-options-sparse2 - - - match: { "test-index-options-sparse2.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } - - match: { "test-index-options-sparse2.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 5.0 } - - match: { "test-index-options-sparse2.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.4 } - - - do: - indices.get_field_mapping: - index: test-index-options-sparse2 - fields: semantic_field - include_defaults: true - - - 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 } From 70a80a4c06b4d9cf25efae4a19ac52894123344e Mon Sep 17 00:00:00 2001 From: Mridula Date: Tue, 7 Oct 2025 20:40:00 +0100 Subject: [PATCH 20/28] Restored error --- ...search.inference.InferenceServiceExtension | 2 +- .../10_semantic_text_field_mapping.yml | 1481 +++++++++++++++++ 2 files changed, 1482 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension index 4de94117c902b..a481e2a4a0451 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension @@ -2,4 +2,4 @@ org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension org.elasticsearch.xpack.inference.mock.TestRerankingServiceExtension org.elasticsearch.xpack.inference.mock.TestStreamingCompletionServiceExtension -org.elasticsearch.xpack.inference.mock.TestCompletionServiceExtension \ No newline at end of file +org.elasticsearch.xpack.inference.mock.TestCompletionServiceExtension 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 new file mode 100644 index 0000000000000..88f20d0c5fa6d --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml @@ -0,0 +1,1481 @@ +setup: + - requires: + cluster_features: "gte_v8.15.0" + reason: semantic_text introduced in 8.15.0 + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id + body: > + { + "service": "test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + inference.put: + task_type: text_embedding + inference_id: dense-inference-id + body: > + { + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_model", + "dimensions": 4, + "similarity": "cosine", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + inference.put: + task_type: text_embedding + inference_id: dense-inference-id-compatible-with-bbq + body: > + { + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_model", + "dimensions": 64, + "similarity": "cosine", + "api_key": "abc64" + }, + "task_settings": { + } + } + + + - do: + indices.create: + index: test-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + sparse_field: + type: semantic_text + inference_id: sparse-inference-id + dense_field: + type: semantic_text + inference_id: dense-inference-id + +--- +"Indexes sparse vector document": + # Checks mapping is not updated until first doc arrives + - 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 } + - length: { "test-index.mappings.properties.sparse_field": 2 } + + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.sparse_field: + inference: + inference_id: sparse-inference-id + model_settings: + task_type: sparse_embedding + chunks: + sparse_field: + - start_offset: 0 + end_offset: 44 + embeddings: + feature_0: 1.0 + feature_1: 2.0 + feature_2: 3.0 + feature_3: 4.0 + - start_offset: 44 + end_offset: 67 + embeddings: + feature_4: 0.1 + feature_5: 0.2 + feature_6: 0.3 + feature_7: 0.4 + + # Checks mapping is updated when first doc arrives + - 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.task_type": sparse_embedding } + - length: { "test-index.mappings.properties.sparse_field": 3 } + +--- +"Field caps with sparse embedding": + - requires: + cluster_features: "gte_v8.16.0" + reason: field_caps support for semantic_text added in 8.16.0 + + - do: + field_caps: + include_empty_fields: true + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - exists: fields.sparse_field + - exists: fields.dense_field + + - do: + field_caps: + include_empty_fields: false + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - not_exists: fields.sparse_field + - not_exists: fields.dense_field + + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.sparse_field: + inference: + inference_id: sparse-inference-id + model_settings: + task_type: sparse_embedding + chunks: + sparse_field: + - start_offset: 0 + end_offset: 44 + embeddings: + feature_0: 1.0 + feature_1: 2.0 + feature_2: 3.0 + feature_3: 4.0 + - start_offset: 44 + end_offset: 67 + embeddings: + feature_4: 0.1 + feature_5: 0.2 + feature_6: 0.3 + feature_7: 0.4 + refresh: true + + - do: + field_caps: + include_empty_fields: true + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - exists: fields.sparse_field + - exists: fields.dense_field + - match: { fields.sparse_field.text.searchable: true } + - match: { fields.dense_field.text.searchable: true } + + - do: + field_caps: + include_empty_fields: false + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - exists: fields.sparse_field + - not_exists: fields.dense_field + - match: { fields.sparse_field.text.searchable: true } + +--- +"Indexes dense vector document": + # Checks mapping is not updated until first doc arrives + - 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 } + - not_exists: test-index.mappings.properties.dense_field.model_settings + + - do: + index: + index: test-index + id: doc_2 + body: + dense_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.dense_field: + inference: + inference_id: dense-inference-id + model_settings: + task_type: text_embedding + dimensions: 4 + similarity: cosine + element_type: float + chunks: + dense_field: + - start_offset: 0 + end_offset: 44 + embeddings: [ 0.04673296958208084, -0.03237321600317955, -0.02543032355606556, 0.056035321205854416 ] + - start_offset: 44 + end_offset: 67 + embeddings: [ 0.00641461368650198, -0.0016253676731139421, -0.05126338079571724, 0.053438711911439896 ] + + # Checks mapping is updated when first doc arrives + - 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.task_type": text_embedding } + - exists: test-index.mappings.properties.dense_field.model_settings + +--- +"Indexes dense vector document with bbq compatible model": + - requires: + cluster_features: "semantic_text.index_options" + reason: index_options introduced in 8.19.0 + + - do: + indices.create: + index: test-index-options-with-bbq + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + dense_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + + # Checks vector mapping is not updated until first doc arrives + - do: + indices.get_mapping: + index: test-index-options-with-bbq + + - match: { "test-index-options-with-bbq.mappings.properties.dense_field.type": semantic_text } + - match: { "test-index-options-with-bbq.mappings.properties.dense_field.inference_id": dense-inference-id-compatible-with-bbq } + - not_exists: test-index-options-with-bbq.mappings.properties.dense_field.index_options + - not_exists: test-index-options-with-bbq.mappings.properties.dense_field.model_settings + + - do: + index: + index: test-index-options-with-bbq + id: doc_2 + body: + dense_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.dense_field: + inference: + inference_id: dense-inference-id-compatible-with-bbq + model_settings: + task_type: text_embedding + dimensions: 64 + similarity: cosine + element_type: float + chunks: + dense_field: + - start_offset: 0 + end_offset: 44 + embeddings: [ 0.05, -0.03, -0.03, 0.06, 0.01, -0.02, 0.07, 0.02, -0.04, 0.03, 0.00, 0.05, -0.06, 0.04, -0.01, 0.02, -0.05, 0.01, 0.03, -0.02, 0.06, -0.04, 0.00, 0.05, -0.03, 0.02, 0.01, -0.01, 0.04, -0.06, 0.03, 0.02, -0.02, 0.06, -0.01, 0.00, 0.04, -0.05, 0.01, 0.03, -0.04, 0.02, -0.03, 0.05, -0.02, 0.01, 0.03, -0.06, 0.04, 0.00, -0.01, 0.06, -0.03, 0.02, 0.01, -0.04, 0.05, -0.01, 0.00, 0.04, -0.05, 0.02, 0.03, -0.02 ] + - start_offset: 44 + end_offset: 67 + embeddings: [ 0.05, -0.03, -0.03, 0.06, 0.01, -0.02, 0.07, 0.02, -0.04, 0.03, 0.00, 0.05, -0.06, 0.04, -0.01, 0.02, -0.05, 0.01, 0.03, -0.02, 0.06, -0.04, 0.00, 0.05, -0.03, 0.02, 0.01, -0.01, 0.04, -0.06, 0.03, 0.02, -0.02, 0.06, -0.01, 0.00, 0.04, -0.05, 0.01, 0.03, -0.04, 0.02, -0.03, 0.05, -0.02, 0.01, 0.03, -0.06, 0.04, 0.00, -0.01, 0.06, -0.03, 0.02, 0.01, -0.04, 0.05, -0.01, 0.00, 0.04, -0.05, 0.02, 0.03, -0.02 ] + + + # Checks mapping is updated when first doc arrives + - do: + indices.get_mapping: + index: test-index-options-with-bbq + + - match: { "test-index-options-with-bbq.mappings.properties.dense_field.type": semantic_text } + - match: { "test-index-options-with-bbq.mappings.properties.dense_field.inference_id": dense-inference-id-compatible-with-bbq } + - match: { "test-index-options-with-bbq.mappings.properties.dense_field.model_settings.task_type": text_embedding } + - not_exists: test-index-options-with-bbq.mappings.properties.dense_field.index_options + +--- +"Field caps with text embedding": + - requires: + cluster_features: "gte_v8.16.0" + reason: field_caps support for semantic_text added in 8.16.0 + + - do: + field_caps: + include_empty_fields: true + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - exists: fields.sparse_field + - exists: fields.dense_field + + - do: + field_caps: + include_empty_fields: false + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - not_exists: fields.sparse_field + - not_exists: fields.dense_field + + - do: + index: + index: test-index + id: doc_2 + body: + dense_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.dense_field: + inference: + inference_id: dense-inference-id + model_settings: + task_type: text_embedding + dimensions: 4 + similarity: cosine + element_type: float + chunks: + dense_field: + - start_offset: 0 + end_offset: 44 + embeddings: [ 0.04673296958208084, -0.03237321600317955, -0.02543032355606556, 0.056035321205854416 ] + - start_offset: 44 + end_offset: 67 + embeddings: [ 0.00641461368650198, -0.0016253676731139421, -0.05126338079571724, 0.053438711911439896 ] + refresh: true + + - do: + field_caps: + include_empty_fields: true + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - exists: fields.sparse_field + - exists: fields.dense_field + - match: { fields.sparse_field.text.searchable: true } + - match: { fields.dense_field.text.searchable: true } + + - do: + field_caps: + include_empty_fields: false + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - not_exists: fields.sparse_field + - exists: fields.dense_field + - match: { fields.dense_field.text.searchable: true } + +--- +"Cannot be used directly as a nested field": + + - do: + catch: /semantic_text field \[nested.semantic\] cannot be nested/ + indices.create: + index: test-nested-index + body: + mappings: + properties: + nested: + type: nested + properties: + semantic: + type: semantic_text + inference_id: sparse-inference-id + another_field: + type: keyword + +--- +"Cannot be used as a nested field on nested objects": + + - do: + catch: /semantic_text field \[nested.nested_object.semantic\] cannot be nested/ + indices.create: + index: test-nested-index + body: + mappings: + properties: + nested: + type: nested + properties: + nested_object: + type: object + properties: + semantic: + type: semantic_text + inference_id: sparse-inference-id + another_field: + type: keyword + +--- +"Cannot be in an object field with subobjects disabled": + - requires: + cluster_features: "semantic_text.in_object_field_fix" + reason: object field fix added in 8.16.0 & 8.15.4 + + - do: + catch: bad_request + indices.create: + index: test-subobjects-index + body: + mappings: + properties: + level_1: + type: object + properties: + level_2: + type: object + subobjects: false + properties: + sparse_field: + type: semantic_text + inference_id: sparse-inference-id + + - match: { error.type: illegal_argument_exception } + - match: { error.reason: "semantic_text field [level_1.level_2.sparse_field] cannot be in an object field with + subobjects disabled" } + +--- +"Mapping always includes inference ID": + - requires: + cluster_features: "semantic_text.always_emit_inference_id_fix" + reason: always emit inference ID fix added in 8.17.0 + test_runner_features: [ capabilities ] + capabilities: + - method: GET + path: /_inference + capabilities: [ default_elser_2 ] + + - do: + indices.create: + index: test-always-include-inference-id-index + body: + mappings: + properties: + semantic_field: + type: semantic_text + + - do: + indices.get_mapping: + index: test-always-include-inference-id-index + + - exists: test-always-include-inference-id-index.mappings.properties.semantic_field.inference_id + +--- +"Field caps exclude chunks and embedding fields": + - requires: + cluster_features: "semantic_text.exclude_sub_fields_from_field_caps" + reason: field caps api exclude semantic_text subfields from 9.1.0 & 8.19.0 + + - do: + field_caps: + include_empty_fields: true + index: test-index + fields: "*" + + - match: { indices: [ "test-index" ] } + - exists: fields.sparse_field + - exists: fields.dense_field + - not_exists: fields.sparse_field.inference.chunks.embeddings + - not_exists: fields.sparse_field.inference.chunks.offset + - not_exists: fields.sparse_field.inference.chunks + - not_exists: fields.sparse_field.inference + - not_exists: fields.dense_field.inference.chunks.embeddings + - not_exists: fields.dense_field.inference.chunks.offset + - not_exists: fields.dense_field.inference.chunks + - not_exists: fields.dense_field.inference + +--- +"Field caps does not exclude multi-fields under semantic_text": + - requires: + cluster_features: "semantic_text.exclude_sub_fields_from_field_caps" + reason: field caps api exclude semantic_text subfields from 9.1.0 & 8.19.0 + - do: + indices.create: + index: test-multi-field-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + sparse_field: + type: semantic_text + inference_id: sparse-inference-id + fields: + sparse_keyword_field: + type: keyword + dense_field: + type: semantic_text + inference_id: dense-inference-id + fields: + dense_keyword_field: + type: keyword + + - do: + field_caps: + include_empty_fields: true + index: test-multi-field-index + fields: "*" + + - match: { indices: [ "test-multi-field-index" ] } + - exists: fields.sparse_field + - exists: fields.dense_field + - exists: fields.sparse_field\.sparse_keyword_field + - exists: fields.dense_field\.dense_keyword_field + - not_exists: fields.sparse_field.inference.chunks.embeddings + - not_exists: fields.sparse_field.inference.chunks.offset + - not_exists: fields.sparse_field.inference.chunks + - not_exists: fields.sparse_field.inference + - not_exists: fields.dense_field.inference.chunks.embeddings + - not_exists: fields.dense_field.inference.chunks.offset + - not_exists: fields.dense_field.inference.chunks + - not_exists: fields.dense_field.inference + +--- +"Field caps with semantic query does not fail": + - requires: + cluster_features: "semantic_query.filter_field_caps_fix" + reason: "fixed bug with semantic query filtering in field_caps (#116106)" + # We need at least one document present to exercise can-match phase + - do: + index: + index: test-index + id: doc_1 + body: + sparse_field: "This is a story about a cat and a dog." + refresh: true + + - do: + field_caps: + index: test-index + fields: "*" + body: + index_filter: + semantic: + field: "sparse_field" + query: "test" + + - match: { indices: [ "test-index" ] } + - match: { fields.sparse_field.text.searchable: true } + +--- +"Users can set dense vector index options and index documents using those options": + - requires: + cluster_features: "semantic_text.index_options" + reason: Index options introduced in 8.19.0 + + - do: + indices.create: + index: test-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + type: int8_hnsw + m: 20 + ef_construction: 100 + confidence_interval: 1.0 + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": "int8_hnsw" } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 20 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } + + - do: + index: + index: test-index-options + id: doc_1 + body: + semantic_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.semantic_field: + inference: + inference_id: dense-inference-id + model_settings: + task_type: text_embedding + dimensions: 4 + similarity: cosine + element_type: float + chunks: + semantic_field: + - start_offset: 0 + end_offset: 44 + embeddings: [ 0.04673296958208084, -0.03237321600317955, -0.02543032355606556, 0.056035321205854416 ] + - start_offset: 44 + end_offset: 67 + embeddings: [ 0.00641461368650198, -0.0016253676731139421, -0.05126338079571724, 0.053438711911439896 ] + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": int8_hnsw } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 20 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } + +--- +"Specifying incompatible dense vector index options will fail": + - requires: + cluster_features: "semantic_text.index_options" + reason: Index options introduced in 8.19.0 + + - do: + catch: /unsupported parameters/ + indices.create: + index: test-incompatible-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + type: bbq_flat + ef_construction: 100 + +--- +"Specifying unsupported index option types will fail": + - requires: + cluster_features: "semantic_text.index_options" + reason: Index options introduced in 8.19.0 + + - do: + catch: /Unsupported index options type/ + indices.create: + index: test-invalid-index-options-dense + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + type: foo + - do: + catch: bad_request + indices.create: + index: test-invalid-index-options-sparse + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + index_options: + sparse_vector: + type: int8_hnsw + +--- +"Index option type is required": + - requires: + cluster_features: "semantic_text.index_options" + reason: Index options introduced in 8.19.0 + + - do: + catch: /Required type/ + indices.create: + index: test-invalid-index-options-dense + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + foo: bar + +--- +"Specifying index options requires model information": + - requires: + cluster_features: "semantic_text.index_options" + reason: Index options introduced in 8.19.0 + + - do: + catch: /Model settings must be set to validate index options/ + indices.create: + index: my-custom-semantic-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: nonexistent-inference-id + index_options: + dense_vector: + type: int8_hnsw + + - match: { status: 400 } + + - do: + indices.create: + index: my-custom-semantic-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: nonexistent-inference-id + + - do: + indices.get_mapping: + index: my-custom-semantic-index + + - match: { "my-custom-semantic-index.mappings.properties.semantic_field.type": semantic_text } + - match: { "my-custom-semantic-index.mappings.properties.semantic_field.inference_id": nonexistent-inference-id } + - not_exists: my-custom-semantic-index.mappings.properties.semantic_field.index_options + +--- +"Updating index options": + - requires: + cluster_features: "semantic_text.index_options" + reason: Index options introduced in 8.19.0 + + - do: + indices.create: + index: test-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + type: int8_hnsw + m: 16 + ef_construction: 100 + confidence_interval: 1.0 + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": "int8_hnsw" } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } + + - do: + indices.put_mapping: + index: test-index-options + body: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + type: int8_hnsw + m: 20 + ef_construction: 90 + confidence_interval: 1.0 + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.type": "int8_hnsw" } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.m": 20 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 90 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.dense_vector.confidence_interval": 1.0 } + + - do: + catch: /Cannot update parameter \[index_options\]/ + indices.put_mapping: + index: test-index-options + body: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + dense_vector: + type: int8_flat + + - match: { status: 400 } + + +--- +"Displaying default index_options with and without include_defaults": + - requires: + cluster_features: "semantic_text.index_options_with_defaults" + reason: Index options defaults support introduced in 9.2.0 + + # Semantic text defaults to BBQ HNSW starting in 8.19.0/9.1.0 + - do: + indices.create: + index: test-index-options-dense + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + + - do: + indices.get_mapping: + index: test-index-options-dense + + - not_exists: test-index-options-dense.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-dense + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + # Validate that actually specifying the same values as our defaults will still serialize the user provided index_options + - do: + indices.create: + index: test-index-options-dense2 + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + index_options: + dense_vector: + type: bbq_hnsw + m: 16 + ef_construction: 100 + rescore_vector: + oversample: 3 + + - do: + indices.get_mapping: + index: test-index-options-dense2 + + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + - do: + indices.get_field_mapping: + index: test-index-options-dense2 + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + # Indices not compatible with BBQ for whatever reason will fall back to whatever `dense_vector` defaults are. + - do: + indices.create: + index: test-index-options-dense-no-bbq + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + + - do: + indices.get_mapping: + index: test-index-options-dense-no-bbq + + - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-dense-no-bbq + fields: semantic_field + include_defaults: true + + - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options + + # Sparse embeddings models do not have index options for semantic_text in 8.19/9.1. + - do: + indices.create: + index: test-index-options-sparse + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + + - do: + indices.get_mapping: + index: test-index-options-sparse + + - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-sparse + fields: semantic_field + include_defaults: true + + - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options + +--- +"Users can set sparse vector index options and index documents using those options": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + indices.create: + index: test-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: true + pruning_config: + tokens_freq_ratio_threshold: 18.0 + tokens_weight_threshold: 0.6 + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 18.0 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.6 } + + - do: + index: + index: test-index-options + id: doc_1 + body: + semantic_field: "these are not the droids you're looking for. He's free to go around" + _inference_fields.semantic_field: + inference: + inference_id: sparse-inference-id + model_settings: + task_type: sparse_embedding + chunks: + semantic_field: + - start_offset: 0 + end_offset: 44 + embeddings: + dr: 1.6103356 + these: 1.1396849 + - start_offset: 44 + end_offset: 67 + embeddings: + free: 1.693662 + around: 1.4376559 + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 18.0 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.6 } + +--- +"Specifying invalid sparse vector index options will fail": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + catch: /\[index_options\] unknown field \[ef_construction\]/ + indices.create: + index: test-incompatible-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + ef_construction: 100 + + - match: { status: 400 } + + - do: + catch: /\[index_options\] field \[pruning_config\] should only be set if \[prune\] is set to true/ + indices.create: + index: test-incompatible-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: false + pruning_config: + tokens_freq_ratio_threshold: 18.0 + tokens_weight_threshold: 0.6 + + - match: { status: 400 } + + - do: + catch: /\[tokens_freq_ratio_threshold\] must be between \[1\] and \[100\]/ + indices.create: + index: test-incompatible-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: true + pruning_config: + tokens_freq_ratio_threshold: 101 + tokens_weight_threshold: 0.6 + + - match: { status: 400 } + + - do: + catch: /unknown field \[some_other_param\]/ + indices.create: + index: test-incompatible-index-options + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: true + pruning_config: + tokens_freq_ratio_threshold: 18.0 + tokens_weight_threshold: 0.6 + some_other_param: true + + - match: { status: 400 } + +--- +"Specifying sparse vector index options should fail using dense index options": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + catch: /Invalid task type for index options/ + indices.create: + index: my-custom-semantic-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + dense_vector: + type: bbq_hnsw + m: 16 + ef_construction: 100 + + - match: { status: 400 } + +--- +"Specifying dense vector index options should fail using sparse index options": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + catch: /Invalid task type for index options/ + indices.create: + index: my-custom-semantic-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + index_options: + sparse_vector: + prune: false + + - match: { status: 400 } + +--- +"Specifying sparse vector index options requires sparse vector model": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + catch: /Model settings must be set to validate index options/ + indices.create: + index: should-be-invalid-index + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: nonexistent-inference-id + index_options: + sparse_vector: + prune: false + + - match: { status: 400 } + +--- +"Updating sparse vector index options": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + indices.create: + index: test-index-options + body: + settings: + number_of_shards: 1 + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: true + pruning_config: + tokens_freq_ratio_threshold: 1.0 + tokens_weight_threshold: 1.0 + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 1.0 } + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 1.0 } + + - do: + index: + index: test-index-options + id: doc_1 + refresh: true + body: + semantic_field: "cheese is comet" + _inference_fields.semantic_field: + inference: + inference_id: sparse-inference-id + model_settings: + task_type: sparse_embedding + chunks: + semantic_field: + - start_offset: 0 + end_offset: 67 + embeddings: + feature_0: 2.671405 + feature_1: 0.11809908 + feature_2: 0.26088917 + + - do: + index: + index: test-index-options + id: doc_2 + refresh: true + body: + semantic_field: "planet is astronomy moon" + _inference_fields.semantic_field: + inference: + inference_id: sparse-inference-id + model_settings: + task_type: sparse_embedding + chunks: + semantic_field: + - start_offset: 0 + end_offset: 67 + embeddings: + feature_0: 2.3438394 + feature_1: 0.54600334 + feature_2: 0.36015007 + feature_3: 0.20022368 + + - do: + index: + index: test-index-options + id: doc_3 + refresh: true + body: + semantic_field: "is globe ocean underground" + _inference_fields.semantic_field: + inference: + inference_id: sparse-inference-id + model_settings: + task_type: sparse_embedding + chunks: + semantic_field: + - start_offset: 0 + end_offset: 67 + embeddings: + feature_0: 0.6891394 + feature_1: 0.484035 + feature_2: 0.080102935 + feature_3: 0.053516876 + + - do: + search: + index: test-index-options + body: + query: + semantic: + field: "semantic_field" + query: "test query" + + - match: { hits.total.value: 2 } + - match: { hits.hits.0._id: "doc_2" } + - match: { hits.hits.1._id: "doc_3" } + + - do: + indices.put_mapping: + index: test-index-options + body: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: false + + - do: + indices.get_mapping: + index: test-index-options + + - match: { "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.prune": false } + - not_exists: "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold" + - not_exists: "test-index-options.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold" + + - do: + search: + index: test-index-options + body: + query: + semantic: + field: "semantic_field" + query: "test query" + + - match: { hits.total.value: 3 } + - match: { hits.hits.0._id: "doc_2" } + - match: { hits.hits.1._id: "doc_1" } + - match: { hits.hits.2._id: "doc_3" } + + +--- +"Displaying default sparse vector index_options with and without include_defaults": + - requires: + cluster_features: "semantic_text.sparse_vector_index_options" + reason: Index options for sparse vector introduced in 9.2.0 + + - do: + indices.create: + index: test-index-options-sparse + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + + - do: + indices.get_mapping: + index: test-index-options-sparse + + - not_exists: test-index-options-sparse.mappings.semantic_field.mapping.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-sparse + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-sparse.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.prune": true } + - match: { "test-index-options-sparse.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 5.0 } + - match: { "test-index-options-sparse.mappings.semantic_field.mapping.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.4 } + + # Validate that actually specifying the same values as our defaults will still serialize the user provided index_options + - do: + indices.create: + index: test-index-options-sparse2 + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + index_options: + sparse_vector: + prune: true + pruning_config: + tokens_freq_ratio_threshold: 5.0 + tokens_weight_threshold: 0.4 + + - do: + indices.get_mapping: + index: test-index-options-sparse2 + + - match: { "test-index-options-sparse2.mappings.properties.semantic_field.index_options.sparse_vector.prune": true } + - match: { "test-index-options-sparse2.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_freq_ratio_threshold": 5.0 } + - match: { "test-index-options-sparse2.mappings.properties.semantic_field.index_options.sparse_vector.pruning_config.tokens_weight_threshold": 0.4 } + + - do: + indices.get_field_mapping: + index: test-index-options-sparse2 + fields: semantic_field + include_defaults: true + + - 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 } From 9564444b7e02e16fe4df956fb3ed2a819615b353 Mon Sep 17 00:00:00 2001 From: Mridula Date: Tue, 7 Oct 2025 20:48:11 +0100 Subject: [PATCH 21/28] Update docs/changelog/134708.yaml --- docs/changelog/134708.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/134708.yaml diff --git a/docs/changelog/134708.yaml b/docs/changelog/134708.yaml new file mode 100644 index 0000000000000..3cdd7f1612309 --- /dev/null +++ b/docs/changelog/134708.yaml @@ -0,0 +1,5 @@ +pr: 134708 +summary: Default `semantic_text` fields to ELSER on EIS when available +area: Relevance +type: enhancement +issues: [] From 0a678ed25be19d1a7b9227cc018d2b502c70bcba Mon Sep 17 00:00:00 2001 From: Mridula Date: Wed, 8 Oct 2025 12:30:44 +0100 Subject: [PATCH 22/28] Integration test --- .../inference/InferenceSemanticTextIT.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java new file mode 100644 index 0000000000000..e9b197c2c4cc3 --- /dev/null +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. + * Licensed under the Elastic License 2.0; you may not use this file except + * in compliance with the Elastic License 2.0. + */ +package org.elasticsearch.xpack.inference; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.rest.ObjectPath; +import org.junit.Before; + +import java.io.IOException; + +import java.util.Map; + + + +import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; +import static org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalService.DEFAULT_ELSER_ID; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Simple end-to-end test that verifies Elasticsearch automatically assigns the + * default ELSER inference endpoint ID to a {@code semantic_text} field mapping + * when none is provided by the user. + */ +public class InferenceSemanticTextIT extends BaseMockEISAuthServerTest { + + @Before + public void setUpMockServer() throws Exception { + super.setUp(); + mockEISServer.enqueueAuthorizeAllModelsResponse(); + } + + public void testDefaultInferenceIdForSemanticText() throws IOException { + String indexName = "semantic-index"; + String mapping = """ + { + \"properties\": { + \"semantic_text_field\": { + \"type\": \"semantic_text\" + } + } + } + """; + Settings settings = Settings.builder().build(); + createIndex(indexName, settings, mapping); + + Map mappingAsMap = getIndexMappingAsMap(indexName); + ObjectPath path = new ObjectPath(mappingAsMap); + String populatedInferenceId = path.evaluate("properties.semantic_text_field.inference_id"); + assertThat("[inference_id] should be auto-populated", populatedInferenceId, notNullValue()); + + assertThat( + populatedInferenceId, + anyOf(equalTo(DEFAULT_ELSER_ID), equalTo(DEFAULT_ELSER_ENDPOINT_ID_V2)) + ); + + + } + +} From d23256bc8d17485f311440c37b1d8ef01649f352 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 8 Oct 2025 11:38:23 +0000 Subject: [PATCH 23/28] [CI] Auto commit changes from spotless --- .../xpack/inference/InferenceSemanticTextIT.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java index e9b197c2c4cc3..f7a0d5fa74b5c 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java @@ -10,11 +10,8 @@ import org.junit.Before; import java.io.IOException; - import java.util.Map; - - import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; import static org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalService.DEFAULT_ELSER_ID; import static org.hamcrest.Matchers.anyOf; @@ -53,11 +50,7 @@ public void testDefaultInferenceIdForSemanticText() throws IOException { String populatedInferenceId = path.evaluate("properties.semantic_text_field.inference_id"); assertThat("[inference_id] should be auto-populated", populatedInferenceId, notNullValue()); - assertThat( - populatedInferenceId, - anyOf(equalTo(DEFAULT_ELSER_ID), equalTo(DEFAULT_ELSER_ENDPOINT_ID_V2)) - ); - + assertThat(populatedInferenceId, anyOf(equalTo(DEFAULT_ELSER_ID), equalTo(DEFAULT_ELSER_ENDPOINT_ID_V2))); } From 0edfb919f6c13bb31d2ddd957e9f5a9c4e0377f3 Mon Sep 17 00:00:00 2001 From: Mridula Date: Tue, 14 Oct 2025 20:36:34 +0100 Subject: [PATCH 24/28] Resolved all PR comments --- docs/changelog/134708.yaml | 2 +- .../inference/InferenceSemanticTextIT.java | 57 ------------------- .../inference/SemanticTextEISDefaultIT.java | 50 ++++++++++++++++ .../mapper/SemanticTextFieldMapper.java | 3 - .../mapper/SemanticTextFieldMapperTests.java | 6 +- 5 files changed, 52 insertions(+), 66 deletions(-) delete mode 100644 x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java create mode 100644 x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java diff --git a/docs/changelog/134708.yaml b/docs/changelog/134708.yaml index 3cdd7f1612309..6851ee1f4cf03 100644 --- a/docs/changelog/134708.yaml +++ b/docs/changelog/134708.yaml @@ -1,5 +1,5 @@ pr: 134708 summary: Default `semantic_text` fields to ELSER on EIS when available -area: Relevance +area: Mapping type: enhancement issues: [] diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java deleted file mode 100644 index f7a0d5fa74b5c..0000000000000 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceSemanticTextIT.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. - * Licensed under the Elastic License 2.0; you may not use this file except - * in compliance with the Elastic License 2.0. - */ -package org.elasticsearch.xpack.inference; - -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.rest.ObjectPath; -import org.junit.Before; - -import java.io.IOException; -import java.util.Map; - -import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2; -import static org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalService.DEFAULT_ELSER_ID; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; - -/** - * Simple end-to-end test that verifies Elasticsearch automatically assigns the - * default ELSER inference endpoint ID to a {@code semantic_text} field mapping - * when none is provided by the user. - */ -public class InferenceSemanticTextIT extends BaseMockEISAuthServerTest { - - @Before - public void setUpMockServer() throws Exception { - super.setUp(); - mockEISServer.enqueueAuthorizeAllModelsResponse(); - } - - public void testDefaultInferenceIdForSemanticText() throws IOException { - String indexName = "semantic-index"; - String mapping = """ - { - \"properties\": { - \"semantic_text_field\": { - \"type\": \"semantic_text\" - } - } - } - """; - Settings settings = Settings.builder().build(); - createIndex(indexName, settings, mapping); - - Map mappingAsMap = getIndexMappingAsMap(indexName); - ObjectPath path = new ObjectPath(mappingAsMap); - String populatedInferenceId = path.evaluate("properties.semantic_text_field.inference_id"); - assertThat("[inference_id] should be auto-populated", populatedInferenceId, notNullValue()); - - assertThat(populatedInferenceId, anyOf(equalTo(DEFAULT_ELSER_ID), equalTo(DEFAULT_ELSER_ENDPOINT_ID_V2))); - - } - -} diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java new file mode 100644 index 0000000000000..236a7649b0a6f --- /dev/null +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. + * Licensed under the Elastic License 2.0; you may not use this file except + * in compliance with the Elastic License 2.0. + */ +package org.elasticsearch.xpack.inference; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.support.XContentMapValues; + +import java.io.IOException; +import java.util.Map; + +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID; +import static org.hamcrest.Matchers.equalTo; + +/** + * End-to-end test that verifies semantic_text fields automatically default to ELSER on EIS + * when available and no inference_id is explicitly provided. + */ +public class SemanticTextEISDefaultIT extends BaseMockEISAuthServerTest { + + public void testDefaultInferenceIdForSemanticText() throws IOException { + String indexName = "semantic-index"; + String mapping = """ + { + "properties": { + "semantic_text_field": { + "type": "semantic_text" + } + } + } + """; + Settings settings = Settings.builder().build(); + createIndex(indexName, settings, mapping); + + Map mappingAsMap = getIndexMappingAsMap(indexName); + String populatedInferenceId = (String) XContentMapValues.extractValue( + "properties.semantic_text_field.inference_id", + mappingAsMap + ); + + assertThat( + "semantic_text field should default to ELSER on EIS when available", + populatedInferenceId, + equalTo(DEFAULT_EIS_ELSER_INFERENCE_ID) + ); + } + +} 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 9be25d47bca38..39a8302282b6f 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 @@ -894,9 +894,6 @@ public Query existsQuery(SearchExecutionContext context) { if (modelSettings == null) { return new MatchNoDocsQuery(); } - if (getEmbeddingsField() == null) { - return new MatchNoDocsQuery(); - } return NestedQueryBuilder.toQuery( (c -> getEmbeddingsField().fieldType().existsQuery(c)), 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 f5f318484c240..1cf394e0e9e3b 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 @@ -321,8 +321,6 @@ public void testDefaultInferenceIdUsesEisWhenAvailable() throws Exception { MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID); - DocumentMapper mapper = mapperService.documentMapper(); - assertThat(mapper.mappingSource().toString(), containsString("\"inference_id\":\"" + DEFAULT_EIS_ELSER_INFERENCE_ID + "\"")); } public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception { @@ -333,14 +331,12 @@ public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat); assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID); - DocumentMapper mapper = mapperService.documentMapper(); - assertThat(mapper.mappingSource().toString(), containsString("\"inference_id\":\"" + DEFAULT_FALLBACK_ELSER_INFERENCE_ID + "\"")); } private void removeDefaultEisEndpoint() { PlainActionFuture removalFuture = new PlainActionFuture<>(); globalModelRegistry.removeDefaultConfigs(Set.of(DEFAULT_EIS_ELSER_INFERENCE_ID), removalFuture); - assertTrue("Failed to remove default EIS endpoint", removalFuture.actionGet()); + assertTrue("Failed to remove default EIS endpoint", removalFuture.actionGet(TEST_REQUEST_TIMEOUT)); } @Override From dd765ae4e6e3ddfc9b13753e685fecc4e3ae7f0d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 14 Oct 2025 19:48:21 +0000 Subject: [PATCH 25/28] [CI] Auto commit changes from spotless --- .../xpack/inference/SemanticTextEISDefaultIT.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java index 236a7649b0a6f..3e8136c95a26c 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java @@ -35,10 +35,7 @@ public void testDefaultInferenceIdForSemanticText() throws IOException { createIndex(indexName, settings, mapping); Map mappingAsMap = getIndexMappingAsMap(indexName); - String populatedInferenceId = (String) XContentMapValues.extractValue( - "properties.semantic_text_field.inference_id", - mappingAsMap - ); + String populatedInferenceId = (String) XContentMapValues.extractValue("properties.semantic_text_field.inference_id", mappingAsMap); assertThat( "semantic_text field should default to ELSER on EIS when available", From 536d32672eff156bd39fb3cf7580763166b06495 Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 17 Oct 2025 11:43:31 +0100 Subject: [PATCH 26/28] Cleaned up the redudant reference of TestInferencePlugin --- .../xpack/inference/mapper/SemanticTextFieldMapperTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1cf394e0e9e3b..1ad96223f0403 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 @@ -172,7 +172,7 @@ protected Collection getPlugins() { protected Supplier getModelRegistry() { return () -> globalModelRegistry; } - }, new XPackClientPlugin(), new TestInferenceServicePlugin()); + }, new XPackClientPlugin()); } private void registerDefaultEisEndpoint() { From cd9872766b0f192f7f5db48369374f0f72ae52a4 Mon Sep 17 00:00:00 2001 From: Mridula Date: Fri, 17 Oct 2025 12:10:47 +0100 Subject: [PATCH 27/28] Included both before and before test --- .../inference/SemanticTextEISDefaultIT.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java index 3e8136c95a26c..a4fc22ca4e2fc 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/SemanticTextEISDefaultIT.java @@ -7,6 +7,8 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.junit.Before; +import org.junit.BeforeClass; import java.io.IOException; import java.util.Map; @@ -20,6 +22,28 @@ */ public class SemanticTextEISDefaultIT extends BaseMockEISAuthServerTest { + @Before + public void setUp() throws Exception { + super.setUp(); + // Ensure the mock EIS server has an authorized response ready before each test + mockEISServer.enqueueAuthorizeAllModelsResponse(); + } + + /** + * This is done before the class because I've run into issues where another class that extends {@link BaseMockEISAuthServerTest} + * results in an authorization response not being queued up for the new Elasticsearch Node in time. When the node starts up, it + * retrieves authorization. If the request isn't queued up when that happens the tests will fail. From my testing locally it seems + * like the base class's static functionality to queue a response is only done once and not for each subclass. + * + * My understanding is that the @Before will be run after the node starts up and wouldn't be sufficient to handle + * this scenario. That is why this needs to be @BeforeClass. + */ + @BeforeClass + public static void init() { + // Ensure the mock EIS server has an authorized response ready + mockEISServer.enqueueAuthorizeAllModelsResponse(); + } + public void testDefaultInferenceIdForSemanticText() throws IOException { String indexName = "semantic-index"; String mapping = """ From cf797e4684a2f8c4b3cbce49568d4f97429edf97 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 17 Oct 2025 11:18:26 +0000 Subject: [PATCH 28/28] [CI] Auto commit changes from spotless --- .../xpack/inference/mapper/SemanticTextFieldMapperTests.java | 1 - 1 file changed, 1 deletion(-) 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 1ad96223f0403..f1d2d3053397d 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 @@ -83,7 +83,6 @@ import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.inference.InferencePlugin; -import org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin; import org.elasticsearch.xpack.inference.model.TestModel; import org.elasticsearch.xpack.inference.registry.ModelRegistry; import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService;