Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
328d079
Defaulting EIS on ELSER
mridula-s109 Sep 15, 2025
0b89373
Extended testing
mridula-s109 Sep 15, 2025
1cc9fd4
[CI] Auto commit changes from spotless
Sep 15, 2025
0829058
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Sep 17, 2025
7dcd31f
Edited to include the default
mridula-s109 Sep 17, 2025
f19972e
Cleaned up the mapper implementation
mridula-s109 Sep 17, 2025
3d58a50
COmpile issue
mridula-s109 Sep 17, 2025
394bf99
[CI] Auto commit changes from spotless
Sep 17, 2025
213aa24
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Sep 25, 2025
c92d2b3
Added tests
mridula-s109 Sep 30, 2025
1301a43
[CI] Auto commit changes from spotless
Sep 30, 2025
6ecdb6c
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Oct 1, 2025
9f662ce
Refactored the variable names
mridula-s109 Oct 2, 2025
904a242
Cleanup done
mridula-s109 Oct 2, 2025
6355bed
Removed unnecessary files
mridula-s109 Oct 2, 2025
b4ed763
Unit tests and mock is working
mridula-s109 Oct 3, 2025
4f0d7c0
[CI] Auto commit changes from spotless
Oct 3, 2025
4680417
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Oct 3, 2025
18f38b8
Fix test
mridula-s109 Oct 3, 2025
cc2e978
yaml addition failure
mridula-s109 Oct 3, 2025
33f4eaa
[CI] Auto commit changes from spotless
Oct 3, 2025
6ef7543
Resolved the import issue or duplication of variables in mock
mridula-s109 Oct 6, 2025
7733cfe
Resolved PR comments
mridula-s109 Oct 7, 2025
70a80a4
Restored error
mridula-s109 Oct 7, 2025
9564444
Update docs/changelog/134708.yaml
mridula-s109 Oct 7, 2025
0a678ed
Integration test
mridula-s109 Oct 8, 2025
d23256b
[CI] Auto commit changes from spotless
Oct 8, 2025
f0fc256
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Oct 8, 2025
0edfb91
Resolved all PR comments
mridula-s109 Oct 14, 2025
f3c0da5
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Oct 14, 2025
dd765ae
[CI] Auto commit changes from spotless
Oct 14, 2025
5d932ef
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Oct 14, 2025
536d326
Cleaned up the redudant reference of TestInferencePlugin
mridula-s109 Oct 17, 2025
cd98727
Included both before and before test
mridula-s109 Oct 17, 2025
cf797e4
[CI] Auto commit changes from spotless
Oct 17, 2025
9e50bdc
Merge branch 'main' into default_elser_on_eis_semantic
mridula-s109 Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/134708.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 134708
summary: Default `semantic_text` fields to ELSER on EIS when available
area: Mapping
type: enhancement
issues: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 org.junit.Before;
import org.junit.BeforeClass;

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 {

@Before
public void setUp() throws Exception {
super.setUp();
// Ensure the mock EIS server has an authorized response ready before each test
mockEISServer.enqueueAuthorizeAllModelsResponse();
}
Comment on lines +25 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my understanding of the conversation in #134708 (comment) (and the linked references), I think we only need the @BeforeClass annotated method. However, this additional @Before method will be harmless at worst, so nothing to block over. As @ioanatia said earlier, we can sync with the ML team and clean this up later.


/**
* 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.
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially unsure about keeping this comment due to potential redundancy, but decided to leave it as-is since the added verbosity makes it more explanatory.

@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 = """
{
"properties": {
"semantic_text_field": {
"type": "semantic_text"
}
}
}
""";
Settings settings = Settings.builder().build();
createIndex(indexName, settings, mapping);

Map<String, Object> 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)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -121,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;

/**
Expand Down Expand Up @@ -152,7 +153,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;
public static final String DEFAULT_FALLBACK_ELSER_INFERENCE_ID = DEFAULT_ELSER_ID;
public static final String DEFAULT_EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2;

public static final String UNSUPPORTED_INDEX_MESSAGE = "["
+ CONTENT_TYPE
Expand All @@ -162,6 +164,18 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie

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(DEFAULT_EIS_ELSER_INFERENCE_ID)) {
return DEFAULT_EIS_ELSER_INFERENCE_ID;
}
return DEFAULT_FALLBACK_ELSER_INFERENCE_ID;
}

static final String INDEX_OPTIONS_FIELD = "index_options";

public static final TypeParser parser(Supplier<ModelRegistry> modelRegistry) {
Expand Down Expand Up @@ -245,7 +259,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(
Expand Down Expand Up @@ -876,7 +890,8 @@ public Query termQuery(Object value, SearchExecutionContext context) {

@Override
public Query existsQuery(SearchExecutionContext context) {
if (getEmbeddingsField() == null) {
// If this field has never seen inference results (no model settings), there are no values yet
if (modelSettings == null) {
return new MatchNoDocsQuery();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,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;
Expand Down Expand Up @@ -63,6 +64,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;
Expand All @@ -83,6 +85,7 @@
import org.elasticsearch.xpack.inference.InferencePlugin;
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;
Expand All @@ -109,7 +112,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_ELSER_2_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;
Expand All @@ -120,6 +124,7 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

Expand All @@ -135,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));
Expand All @@ -146,6 +151,7 @@ public boolean localNodeMaster() {
return false;
}
});
registerDefaultEisEndpoint();
}

@After
Expand All @@ -168,6 +174,16 @@ protected Supplier<ModelRegistry> getModelRegistry() {
}, new XPackClientPlugin());
}

private void registerDefaultEisEndpoint() {
globalModelRegistry.putDefaultIdIfAbsent(
new InferenceService.DefaultConfigId(
DEFAULT_EIS_ELSER_INFERENCE_ID,
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);
Expand Down Expand Up @@ -222,7 +238,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_EIS_ELSER_INFERENCE_ID);
}

@Override
Expand Down Expand Up @@ -289,7 +305,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_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID);

ParsedDocument doc1 = mapper.parse(source(this::writeField));
List<IndexableField> fields = doc1.rootDoc().getFields("field");
Expand All @@ -298,6 +314,30 @@ 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_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID);
}

public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception {
final String fieldName = "field";
final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping);

removeDefaultEisEndpoint();

MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat);
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID);
}

private void removeDefaultEisEndpoint() {
PlainActionFuture<Boolean> removalFuture = new PlainActionFuture<>();
globalModelRegistry.removeDefaultConfigs(Set.of(DEFAULT_EIS_ELSER_INFERENCE_ID), removalFuture);
assertTrue("Failed to remove default EIS endpoint", removalFuture.actionGet(TEST_REQUEST_TIMEOUT));
}

@Override
public void testFieldHasValue() {
MappedFieldType fieldType = getMappedFieldType();
Expand Down Expand Up @@ -328,12 +368,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_EIS_ELSER_INFERENCE_ID, searchInferenceId);
assertSerialization.accept(expectedMapping, mapperService);
}
{
Expand Down