Skip to content

Commit 6b2c55a

Browse files
mridula-s109elasticsearchmachine
andauthored
Default semantic_text fields to ELSER on EIS when available (#134708)
* Defaulting EIS on ELSER * Extended testing * [CI] Auto commit changes from spotless * Edited to include the default * Cleaned up the mapper implementation * COmpile issue * [CI] Auto commit changes from spotless * Added tests * [CI] Auto commit changes from spotless * Refactored the variable names * Cleanup done * Removed unnecessary files * Unit tests and mock is working * [CI] Auto commit changes from spotless * Fix test * yaml addition failure * [CI] Auto commit changes from spotless * Resolved the import issue or duplication of variables in mock * Resolved PR comments * Restored error * Update docs/changelog/134708.yaml * Integration test * [CI] Auto commit changes from spotless * Resolved all PR comments * [CI] Auto commit changes from spotless * Cleaned up the redudant reference of TestInferencePlugin * Included both before and before test * [CI] Auto commit changes from spotless * Made changes to accomodate the old constant version string --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 330c666 commit 6b2c55a

File tree

5 files changed

+146
-10
lines changed

5 files changed

+146
-10
lines changed

docs/changelog/134708.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 134708
2+
summary: Default `semantic_text` fields to ELSER on EIS when available
3+
area: Mapping
4+
type: enhancement
5+
issues: []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V.
3+
* Licensed under the Elastic License 2.0; you may not use this file except
4+
* in compliance with the Elastic License 2.0.
5+
*/
6+
package org.elasticsearch.xpack.inference;
7+
8+
import org.elasticsearch.common.settings.Settings;
9+
import org.elasticsearch.common.xcontent.support.XContentMapValues;
10+
import org.junit.Before;
11+
import org.junit.BeforeClass;
12+
13+
import java.io.IOException;
14+
import java.util.Map;
15+
16+
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID;
17+
import static org.hamcrest.Matchers.equalTo;
18+
19+
/**
20+
* End-to-end test that verifies semantic_text fields automatically default to ELSER on EIS
21+
* when available and no inference_id is explicitly provided.
22+
*/
23+
public class SemanticTextEISDefaultIT extends BaseMockEISAuthServerTest {
24+
25+
@Before
26+
public void setUp() throws Exception {
27+
super.setUp();
28+
// Ensure the mock EIS server has an authorized response ready before each test
29+
mockEISServer.enqueueAuthorizeAllModelsResponse();
30+
}
31+
32+
/**
33+
* This is done before the class because I've run into issues where another class that extends {@link BaseMockEISAuthServerTest}
34+
* results in an authorization response not being queued up for the new Elasticsearch Node in time. When the node starts up, it
35+
* retrieves authorization. If the request isn't queued up when that happens the tests will fail. From my testing locally it seems
36+
* like the base class's static functionality to queue a response is only done once and not for each subclass.
37+
*
38+
* My understanding is that the @Before will be run after the node starts up and wouldn't be sufficient to handle
39+
* this scenario. That is why this needs to be @BeforeClass.
40+
*/
41+
@BeforeClass
42+
public static void init() {
43+
// Ensure the mock EIS server has an authorized response ready
44+
mockEISServer.enqueueAuthorizeAllModelsResponse();
45+
}
46+
47+
public void testDefaultInferenceIdForSemanticText() throws IOException {
48+
String indexName = "semantic-index";
49+
String mapping = """
50+
{
51+
"properties": {
52+
"semantic_text_field": {
53+
"type": "semantic_text"
54+
}
55+
}
56+
}
57+
""";
58+
Settings settings = Settings.builder().build();
59+
createIndex(indexName, settings, mapping);
60+
61+
Map<String, Object> mappingAsMap = getIndexMappingAsMap(indexName);
62+
String populatedInferenceId = (String) XContentMapValues.extractValue("properties.semantic_text_field.inference_id", mappingAsMap);
63+
64+
assertThat(
65+
"semantic_text field should default to ELSER on EIS when available",
66+
populatedInferenceId,
67+
equalTo(DEFAULT_EIS_ELSER_INFERENCE_ID)
68+
);
69+
}
70+
71+
}

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName;
122122
import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getOffsetsFieldName;
123123
import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getOriginalTextFieldName;
124+
import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService.DEFAULT_ELSER_ENDPOINT_ID_V2;
124125
import static org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalService.DEFAULT_ELSER_ID;
125126

126127
/**
@@ -153,8 +154,13 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie
153154
public static final NodeFeature SEMANTIC_TEXT_UPDATABLE_INFERENCE_ID = new NodeFeature("semantic_text.updatable_inference_id");
154155

155156
public static final String CONTENT_TYPE = "semantic_text";
156-
public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID;
157-
157+
public static final String DEFAULT_FALLBACK_ELSER_INFERENCE_ID = DEFAULT_ELSER_ID;
158+
public static final String DEFAULT_EIS_ELSER_INFERENCE_ID = DEFAULT_ELSER_ENDPOINT_ID_V2;
159+
/**
160+
* @deprecated Replaced by {@link #DEFAULT_EIS_ELSER_INFERENCE_ID}.
161+
*/
162+
@Deprecated(since = "9.3.0", forRemoval = false)
163+
public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_EIS_ELSER_INFERENCE_ID;
158164
public static final String UNSUPPORTED_INDEX_MESSAGE = "["
159165
+ CONTENT_TYPE
160166
+ "] is available on indices created with 8.11 or higher. Please create a new index to use ["
@@ -163,6 +169,18 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie
163169

164170
public static final float DEFAULT_RESCORE_OVERSAMPLE = 3.0f;
165171

172+
/**
173+
* Determines the preferred ELSER inference ID based on EIS availability.
174+
* Returns .elser-2-elastic (EIS) when available, otherwise falls back to .elser-2-elasticsearch (ML nodes).
175+
* This enables automatic selection of EIS for better performance while maintaining compatibility with on-prem deployments.
176+
*/
177+
private static String getPreferredElserInferenceId(ModelRegistry modelRegistry) {
178+
if (modelRegistry != null && modelRegistry.containsDefaultConfigId(DEFAULT_EIS_ELSER_INFERENCE_ID)) {
179+
return DEFAULT_EIS_ELSER_INFERENCE_ID;
180+
}
181+
return DEFAULT_FALLBACK_ELSER_INFERENCE_ID;
182+
}
183+
166184
static final String INDEX_OPTIONS_FIELD = "index_options";
167185

168186
public static final TypeParser parser(Supplier<ModelRegistry> modelRegistry) {
@@ -246,7 +264,7 @@ public Builder(
246264
INFERENCE_ID_FIELD,
247265
true,
248266
mapper -> ((SemanticTextFieldType) mapper.fieldType()).inferenceId,
249-
DEFAULT_ELSER_2_INFERENCE_ID
267+
getPreferredElserInferenceId(modelRegistry)
250268
).addValidator(v -> {
251269
if (Strings.isEmpty(v)) {
252270
throw new IllegalArgumentException(
@@ -926,7 +944,8 @@ public Query termQuery(Object value, SearchExecutionContext context) {
926944

927945
@Override
928946
public Query existsQuery(SearchExecutionContext context) {
929-
if (getEmbeddingsField() == null) {
947+
// If this field has never seen inference results (no model settings), there are no values yet
948+
if (modelSettings == null) {
930949
return new MatchNoDocsQuery();
931950
}
932951

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public class ElasticInferenceService extends SenderService {
114114

115115
// elser-2
116116
static final String DEFAULT_ELSER_2_MODEL_ID = "elser_model_2";
117-
static final String DEFAULT_ELSER_ENDPOINT_ID_V2 = defaultEndpointId("elser-2");
117+
public static final String DEFAULT_ELSER_ENDPOINT_ID_V2 = defaultEndpointId("elser-2");
118118

119119
// multilingual-text-embed
120120
static final String DEFAULT_MULTILINGUAL_EMBED_MODEL_ID = "jina-embeddings-v3";

x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.lucene.search.join.QueryBitSetProducer;
2727
import org.apache.lucene.search.join.ScoreMode;
2828
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
29+
import org.elasticsearch.action.support.PlainActionFuture;
2930
import org.elasticsearch.cluster.ClusterChangedEvent;
3031
import org.elasticsearch.cluster.metadata.IndexMetadata;
3132
import org.elasticsearch.common.CheckedBiConsumer;
@@ -63,6 +64,7 @@
6364
import org.elasticsearch.index.query.SearchExecutionContext;
6465
import org.elasticsearch.index.search.ESToParentBlockJoinQuery;
6566
import org.elasticsearch.inference.ChunkingSettings;
67+
import org.elasticsearch.inference.InferenceService;
6668
import org.elasticsearch.inference.MinimalServiceSettings;
6769
import org.elasticsearch.inference.Model;
6870
import org.elasticsearch.inference.ServiceSettings;
@@ -84,6 +86,7 @@
8486
import org.elasticsearch.xpack.inference.InferencePlugin;
8587
import org.elasticsearch.xpack.inference.model.TestModel;
8688
import org.elasticsearch.xpack.inference.registry.ModelRegistry;
89+
import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceService;
8790
import org.junit.After;
8891
import org.junit.AssumptionViolatedException;
8992
import org.junit.Before;
@@ -111,7 +114,9 @@
111114
import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.TEXT_FIELD;
112115
import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getChunksFieldName;
113116
import static org.elasticsearch.xpack.inference.mapper.SemanticTextField.getEmbeddingsFieldName;
117+
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_EIS_ELSER_INFERENCE_ID;
114118
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_ELSER_2_INFERENCE_ID;
119+
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_FALLBACK_ELSER_INFERENCE_ID;
115120
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.DEFAULT_RESCORE_OVERSAMPLE;
116121
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.INDEX_OPTIONS_FIELD;
117122
import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.UNSUPPORTED_INDEX_MESSAGE;
@@ -124,6 +129,7 @@
124129
import static org.hamcrest.Matchers.is;
125130
import static org.hamcrest.Matchers.not;
126131
import static org.mockito.ArgumentMatchers.anyString;
132+
import static org.mockito.Mockito.mock;
127133
import static org.mockito.Mockito.spy;
128134
import static org.mockito.Mockito.when;
129135

@@ -139,7 +145,7 @@ public SemanticTextFieldMapperTests(boolean useLegacyFormat) {
139145
ModelRegistry globalModelRegistry;
140146

141147
@Before
142-
private void startThreadPool() {
148+
private void initializeTestEnvironment() {
143149
threadPool = createThreadPool();
144150
var clusterService = ClusterServiceUtils.createClusterService(threadPool);
145151
var modelRegistry = new ModelRegistry(clusterService, new NoOpClient(threadPool));
@@ -150,6 +156,7 @@ public boolean localNodeMaster() {
150156
return false;
151157
}
152158
});
159+
registerDefaultEisEndpoint();
153160
}
154161

155162
@After
@@ -172,6 +179,16 @@ protected Supplier<ModelRegistry> getModelRegistry() {
172179
}, new XPackClientPlugin());
173180
}
174181

182+
private void registerDefaultEisEndpoint() {
183+
globalModelRegistry.putDefaultIdIfAbsent(
184+
new InferenceService.DefaultConfigId(
185+
DEFAULT_EIS_ELSER_INFERENCE_ID,
186+
MinimalServiceSettings.sparseEmbedding(ElasticInferenceService.NAME),
187+
mock(InferenceService.class)
188+
)
189+
);
190+
}
191+
175192
private MapperService createMapperService(XContentBuilder mappings, boolean useLegacyFormat) throws IOException {
176193
IndexVersion indexVersion = SemanticInferenceMetadataFieldsMapperTests.getRandomCompatibleIndexVersion(useLegacyFormat);
177194
return createMapperService(mappings, useLegacyFormat, indexVersion, indexVersion);
@@ -226,7 +243,7 @@ protected void minimalMapping(XContentBuilder b) throws IOException {
226243
@Override
227244
protected void metaMapping(XContentBuilder b) throws IOException {
228245
super.metaMapping(b);
229-
b.field(INFERENCE_ID_FIELD, DEFAULT_ELSER_2_INFERENCE_ID);
246+
b.field(INFERENCE_ID_FIELD, DEFAULT_EIS_ELSER_INFERENCE_ID);
230247
}
231248

232249
@Override
@@ -293,7 +310,7 @@ public void testDefaults() throws Exception {
293310
DocumentMapper mapper = mapperService.documentMapper();
294311
assertEquals(Strings.toString(expectedMapping), mapper.mappingSource().toString());
295312
assertSemanticTextField(mapperService, fieldName, false, null, null);
296-
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, DEFAULT_ELSER_2_INFERENCE_ID);
313+
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID);
297314

298315
ParsedDocument doc1 = mapper.parse(source(this::writeField));
299316
List<IndexableField> fields = doc1.rootDoc().getFields("field");
@@ -302,6 +319,30 @@ public void testDefaults() throws Exception {
302319
assertTrue(fields.isEmpty());
303320
}
304321

322+
public void testDefaultInferenceIdUsesEisWhenAvailable() throws Exception {
323+
final String fieldName = "field";
324+
final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping);
325+
326+
MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat);
327+
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, DEFAULT_EIS_ELSER_INFERENCE_ID);
328+
}
329+
330+
public void testDefaultInferenceIdFallsBackWhenEisUnavailable() throws Exception {
331+
final String fieldName = "field";
332+
final XContentBuilder fieldMapping = fieldMapping(this::minimalMapping);
333+
334+
removeDefaultEisEndpoint();
335+
336+
MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat);
337+
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_FALLBACK_ELSER_INFERENCE_ID, DEFAULT_FALLBACK_ELSER_INFERENCE_ID);
338+
}
339+
340+
private void removeDefaultEisEndpoint() {
341+
PlainActionFuture<Boolean> removalFuture = new PlainActionFuture<>();
342+
globalModelRegistry.removeDefaultConfigs(Set.of(DEFAULT_EIS_ELSER_INFERENCE_ID), removalFuture);
343+
assertTrue("Failed to remove default EIS endpoint", removalFuture.actionGet(TEST_REQUEST_TIMEOUT));
344+
}
345+
305346
@Override
306347
public void testFieldHasValue() {
307348
MappedFieldType fieldType = getMappedFieldType();
@@ -332,12 +373,12 @@ public void testSetInferenceEndpoints() throws IOException {
332373
);
333374
final XContentBuilder expectedMapping = fieldMapping(
334375
b -> b.field("type", "semantic_text")
335-
.field(INFERENCE_ID_FIELD, DEFAULT_ELSER_2_INFERENCE_ID)
376+
.field(INFERENCE_ID_FIELD, DEFAULT_EIS_ELSER_INFERENCE_ID)
336377
.field(SEARCH_INFERENCE_ID_FIELD, searchInferenceId)
337378
);
338379
final MapperService mapperService = createMapperService(fieldMapping, useLegacyFormat);
339380
assertSemanticTextField(mapperService, fieldName, false, null, null);
340-
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_ELSER_2_INFERENCE_ID, searchInferenceId);
381+
assertInferenceEndpoints(mapperService, fieldName, DEFAULT_EIS_ELSER_INFERENCE_ID, searchInferenceId);
341382
assertSerialization.accept(expectedMapping, mapperService);
342383
}
343384
{

0 commit comments

Comments
 (0)