Skip to content

Commit 43e6fad

Browse files
authored
Returning ignored fields in the simulate ingest API (#117214)
1 parent 3dc85be commit 43e6fad

File tree

8 files changed

+183
-14
lines changed

8 files changed

+183
-14
lines changed

docs/changelog/117214.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 117214
2+
summary: Returning ignored fields in the simulate ingest API
3+
area: Ingest Node
4+
type: enhancement
5+
issues: []

qa/smoke-test-ingest-with-all-dependencies/src/yamlRestTest/resources/rest-api-spec/test/ingest/80_ingest_simulate.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,3 +1720,59 @@ setup:
17201720
- match: { docs.0.doc._source.foo: 3 }
17211721
- match: { docs.0.doc._source.bar: "some text value" }
17221722
- not_exists: docs.0.doc.error
1723+
1724+
---
1725+
"Test ignored_fields":
1726+
- skip:
1727+
features:
1728+
- headers
1729+
- allowed_warnings
1730+
1731+
- requires:
1732+
cluster_features: ["simulate.ignored.fields"]
1733+
reason: "ingest simulate ignored fields added in 8.18"
1734+
1735+
- do:
1736+
headers:
1737+
Content-Type: application/json
1738+
simulate.ingest:
1739+
index: nonexistent
1740+
body: >
1741+
{
1742+
"docs": [
1743+
{
1744+
"_index": "simulate-test",
1745+
"_id": "y9Es_JIBiw6_GgN-U0qy",
1746+
"_score": 1,
1747+
"_source": {
1748+
"abc": "sfdsfsfdsfsfdsfsfdsfsfdsfsfdsf"
1749+
}
1750+
}
1751+
],
1752+
"index_template_substitutions": {
1753+
"ind_temp": {
1754+
"index_patterns": ["simulate-test"],
1755+
"composed_of": ["simulate-test"]
1756+
}
1757+
},
1758+
"component_template_substitutions": {
1759+
"simulate-test": {
1760+
"template": {
1761+
"mappings": {
1762+
"dynamic": false,
1763+
"properties": {
1764+
"abc": {
1765+
"type": "keyword",
1766+
"ignore_above": 1
1767+
}
1768+
}
1769+
}
1770+
}
1771+
}
1772+
}
1773+
}
1774+
- length: { docs: 1 }
1775+
- match: { docs.0.doc._index: "simulate-test" }
1776+
- match: { docs.0.doc._source.abc: "sfdsfsfdsfsfdsfsfdsfsfdsfsfdsf" }
1777+
- match: { docs.0.doc.ignored_fields: [ {"field": "abc"} ] }
1778+
- not_exists: docs.0.doc.error

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ static TransportVersion def(int id) {
145145
public static final TransportVersion NODE_VERSION_INFORMATION_WITH_MIN_READ_ONLY_INDEX_VERSION = def(8_810_00_0);
146146
public static final TransportVersion ERROR_TRACE_IN_TRANSPORT_HEADER = def(8_811_00_0);
147147
public static final TransportVersion FAILURE_STORE_ENABLED_BY_CLUSTER_SETTING = def(8_812_00_0);
148+
public static final TransportVersion SIMULATE_IGNORED_FIELDS = def(8_813_00_0);
148149

149150
/*
150151
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/action/bulk/BulkFeatures.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Set;
1616

1717
import static org.elasticsearch.action.bulk.TransportSimulateBulkAction.SIMULATE_COMPONENT_TEMPLATE_SUBSTITUTIONS;
18+
import static org.elasticsearch.action.bulk.TransportSimulateBulkAction.SIMULATE_IGNORED_FIELDS;
1819
import static org.elasticsearch.action.bulk.TransportSimulateBulkAction.SIMULATE_INDEX_TEMPLATE_SUBSTITUTIONS;
1920
import static org.elasticsearch.action.bulk.TransportSimulateBulkAction.SIMULATE_MAPPING_ADDITION;
2021
import static org.elasticsearch.action.bulk.TransportSimulateBulkAction.SIMULATE_MAPPING_VALIDATION;
@@ -29,7 +30,8 @@ public Set<NodeFeature> getFeatures() {
2930
SIMULATE_COMPONENT_TEMPLATE_SUBSTITUTIONS,
3031
SIMULATE_INDEX_TEMPLATE_SUBSTITUTIONS,
3132
SIMULATE_MAPPING_ADDITION,
32-
SIMULATE_SUPPORT_NON_TEMPLATE_MAPPING
33+
SIMULATE_SUPPORT_NON_TEMPLATE_MAPPING,
34+
SIMULATE_IGNORED_FIELDS
3335
);
3436
}
3537
}

server/src/main/java/org/elasticsearch/action/bulk/TransportSimulateBulkAction.java

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
package org.elasticsearch.action.bulk;
1111

12+
import org.apache.lucene.document.StringField;
13+
import org.apache.lucene.index.IndexableField;
1214
import org.elasticsearch.action.ActionListener;
1315
import org.elasticsearch.action.DocWriteRequest;
1416
import org.elasticsearch.action.admin.indices.template.post.TransportSimulateIndexTemplateAction;
@@ -33,13 +35,16 @@
3335
import org.elasticsearch.common.util.concurrent.AtomicArray;
3436
import org.elasticsearch.common.xcontent.XContentHelper;
3537
import org.elasticsearch.core.Nullable;
38+
import org.elasticsearch.core.Tuple;
3639
import org.elasticsearch.features.NodeFeature;
3740
import org.elasticsearch.index.IndexSettingProvider;
3841
import org.elasticsearch.index.IndexSettingProviders;
3942
import org.elasticsearch.index.IndexVersion;
4043
import org.elasticsearch.index.IndexingPressure;
4144
import org.elasticsearch.index.VersionType;
4245
import org.elasticsearch.index.engine.Engine;
46+
import org.elasticsearch.index.mapper.IgnoredFieldMapper;
47+
import org.elasticsearch.index.mapper.LuceneDocument;
4348
import org.elasticsearch.index.mapper.MapperService;
4449
import org.elasticsearch.index.mapper.SourceToParse;
4550
import org.elasticsearch.index.seqno.SequenceNumbers;
@@ -60,6 +65,7 @@
6065
import org.elasticsearch.xcontent.XContentType;
6166

6267
import java.io.IOException;
68+
import java.util.Collection;
6369
import java.util.HashMap;
6470
import java.util.List;
6571
import java.util.Map;
@@ -85,6 +91,7 @@ public class TransportSimulateBulkAction extends TransportAbstractBulkAction {
8591
public static final NodeFeature SIMULATE_INDEX_TEMPLATE_SUBSTITUTIONS = new NodeFeature("simulate.index.template.substitutions");
8692
public static final NodeFeature SIMULATE_MAPPING_ADDITION = new NodeFeature("simulate.mapping.addition");
8793
public static final NodeFeature SIMULATE_SUPPORT_NON_TEMPLATE_MAPPING = new NodeFeature("simulate.support.non.template.mapping");
94+
public static final NodeFeature SIMULATE_IGNORED_FIELDS = new NodeFeature("simulate.ignored.fields");
8895
private final IndicesService indicesService;
8996
private final NamedXContentRegistry xContentRegistry;
9097
private final Set<IndexSettingProvider> indexSettingProviders;
@@ -137,12 +144,13 @@ protected void doInternalExecute(
137144
DocWriteRequest<?> docRequest = bulkRequest.requests.get(i);
138145
assert docRequest instanceof IndexRequest : "TransportSimulateBulkAction should only ever be called with IndexRequests";
139146
IndexRequest request = (IndexRequest) docRequest;
140-
Exception mappingValidationException = validateMappings(
147+
Tuple<Collection<String>, Exception> validationResult = validateMappings(
141148
componentTemplateSubstitutions,
142149
indexTemplateSubstitutions,
143150
mappingAddition,
144151
request
145152
);
153+
Exception mappingValidationException = validationResult.v2();
146154
responses.set(
147155
i,
148156
BulkItemResponse.success(
@@ -155,6 +163,7 @@ protected void doInternalExecute(
155163
request.source(),
156164
request.getContentType(),
157165
request.getExecutedPipelines(),
166+
validationResult.v1(),
158167
mappingValidationException
159168
)
160169
)
@@ -168,11 +177,12 @@ protected void doInternalExecute(
168177
/**
169178
* This creates a temporary index with the mappings of the index in the request, and then attempts to index the source from the request
170179
* into it. If there is a mapping exception, that exception is returned. On success the returned exception is null.
171-
* @parem componentTemplateSubstitutions The component template definitions to use in place of existing ones for validation
180+
* @param componentTemplateSubstitutions The component template definitions to use in place of existing ones for validation
172181
* @param request The IndexRequest whose source will be validated against the mapping (if it exists) of its index
173-
* @return a mapping exception if the source does not match the mappings, otherwise null
182+
* @return a Tuple containing: (1) in v1 the names of any fields that would be ignored upon indexing and (2) in v2 the mapping
183+
* exception if the source does not match the mappings, otherwise null
174184
*/
175-
private Exception validateMappings(
185+
private Tuple<Collection<String>, Exception> validateMappings(
176186
Map<String, ComponentTemplate> componentTemplateSubstitutions,
177187
Map<String, ComposableIndexTemplate> indexTemplateSubstitutions,
178188
Map<String, Object> mappingAddition,
@@ -189,6 +199,7 @@ private Exception validateMappings(
189199

190200
ClusterState state = clusterService.state();
191201
Exception mappingValidationException = null;
202+
Collection<String> ignoredFields = List.of();
192203
IndexAbstraction indexAbstraction = state.metadata().getIndicesLookup().get(request.index());
193204
try {
194205
if (indexAbstraction != null
@@ -275,7 +286,7 @@ private Exception validateMappings(
275286
);
276287
CompressedXContent mappings = template.mappings();
277288
CompressedXContent mergedMappings = mergeMappings(mappings, mappingAddition);
278-
validateUpdatedMappings(mappings, mergedMappings, request, sourceToParse);
289+
ignoredFields = validateUpdatedMappings(mappings, mergedMappings, request, sourceToParse);
279290
} else {
280291
List<IndexTemplateMetadata> matchingTemplates = findV1Templates(simulatedState.metadata(), request.index(), false);
281292
if (matchingTemplates.isEmpty() == false) {
@@ -289,7 +300,7 @@ private Exception validateMappings(
289300
xContentRegistry
290301
);
291302
final CompressedXContent combinedMappings = mergeMappings(new CompressedXContent(mappingsMap), mappingAddition);
292-
validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
303+
ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
293304
} else if (indexAbstraction != null && mappingAddition.isEmpty() == false) {
294305
/*
295306
* The index matched no templates of any kind, including the substitutions. But it might have a mapping. So we
@@ -298,35 +309,36 @@ private Exception validateMappings(
298309
MappingMetadata mappingFromIndex = clusterService.state().metadata().index(indexAbstraction.getName()).mapping();
299310
CompressedXContent currentIndexCompressedXContent = mappingFromIndex == null ? null : mappingFromIndex.source();
300311
CompressedXContent combinedMappings = mergeMappings(currentIndexCompressedXContent, mappingAddition);
301-
validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
312+
ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
302313
} else {
303314
/*
304315
* The index matched no templates and had no mapping of its own. If there were component template substitutions
305316
* or index template substitutions, they didn't match anything. So just apply the mapping addition if it exists,
306317
* and validate.
307318
*/
308319
final CompressedXContent combinedMappings = mergeMappings(null, mappingAddition);
309-
validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
320+
ignoredFields = validateUpdatedMappings(null, combinedMappings, request, sourceToParse);
310321
}
311322
}
312323
}
313324
} catch (Exception e) {
314325
mappingValidationException = e;
315326
}
316-
return mappingValidationException;
327+
return Tuple.tuple(ignoredFields, mappingValidationException);
317328
}
318329

319330
/*
320-
* Validates that when updatedMappings are applied
331+
* Validates that when updatedMappings are applied. If any fields would be ignored while indexing, then those field names are returned.
332+
* Otherwise the returned Collection is empty.
321333
*/
322-
private void validateUpdatedMappings(
334+
private Collection<String> validateUpdatedMappings(
323335
@Nullable CompressedXContent originalMappings,
324336
@Nullable CompressedXContent updatedMappings,
325337
IndexRequest request,
326338
SourceToParse sourceToParse
327339
) throws IOException {
328340
if (updatedMappings == null) {
329-
return; // no validation to do
341+
return List.of(); // no validation to do
330342
}
331343
Settings dummySettings = Settings.builder()
332344
.put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current())
@@ -343,7 +355,7 @@ private void validateUpdatedMappings(
343355
.settings(dummySettings)
344356
.putMapping(new MappingMetadata(updatedMappings))
345357
.build();
346-
indicesService.withTempIndexService(originalIndexMetadata, indexService -> {
358+
Engine.Index result = indicesService.withTempIndexService(originalIndexMetadata, indexService -> {
347359
indexService.mapperService().merge(updatedIndexMetadata, MapperService.MergeReason.MAPPING_UPDATE);
348360
return IndexShard.prepareIndex(
349361
indexService.mapperService(),
@@ -360,6 +372,24 @@ private void validateUpdatedMappings(
360372
0
361373
);
362374
});
375+
final Collection<String> ignoredFields;
376+
if (result == null) {
377+
ignoredFields = List.of();
378+
} else {
379+
List<LuceneDocument> luceneDocuments = result.parsedDoc().docs();
380+
assert luceneDocuments == null || luceneDocuments.size() == 1 : "Expected a single lucene document from index attempt";
381+
if (luceneDocuments != null && luceneDocuments.size() == 1) {
382+
ignoredFields = luceneDocuments.getFirst()
383+
.getFields()
384+
.stream()
385+
.filter(field -> field.name().equals(IgnoredFieldMapper.NAME) && field instanceof StringField)
386+
.map(IndexableField::stringValue)
387+
.toList();
388+
} else {
389+
ignoredFields = List.of();
390+
}
391+
}
392+
return ignoredFields;
363393
}
364394

365395
private static CompressedXContent mergeMappings(@Nullable CompressedXContent originalMapping, Map<String, Object> mappingAddition)

server/src/main/java/org/elasticsearch/action/ingest/SimulateIndexResponse.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.elasticsearch.xcontent.XContentType;
2525

2626
import java.io.IOException;
27+
import java.util.Collection;
2728
import java.util.List;
2829

2930
/**
@@ -34,6 +35,7 @@
3435
public class SimulateIndexResponse extends IndexResponse {
3536
private final BytesReference source;
3637
private final XContentType sourceXContentType;
38+
private final Collection<String> ignoredFields;
3739
private final Exception exception;
3840

3941
@SuppressWarnings("this-escape")
@@ -47,6 +49,11 @@ public SimulateIndexResponse(StreamInput in) throws IOException {
4749
} else {
4850
this.exception = null;
4951
}
52+
if (in.getTransportVersion().onOrAfter(TransportVersions.SIMULATE_IGNORED_FIELDS)) {
53+
this.ignoredFields = in.readStringCollectionAsList();
54+
} else {
55+
this.ignoredFields = List.of();
56+
}
5057
}
5158

5259
@SuppressWarnings("this-escape")
@@ -57,6 +64,7 @@ public SimulateIndexResponse(
5764
BytesReference source,
5865
XContentType sourceXContentType,
5966
List<String> pipelines,
67+
Collection<String> ignoredFields,
6068
@Nullable Exception exception
6169
) {
6270
// We don't actually care about most of the IndexResponse fields:
@@ -73,6 +81,7 @@ public SimulateIndexResponse(
7381
this.source = source;
7482
this.sourceXContentType = sourceXContentType;
7583
setShardInfo(ShardInfo.EMPTY);
84+
this.ignoredFields = ignoredFields;
7685
this.exception = exception;
7786
}
7887

@@ -84,6 +93,16 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t
8493
builder.field("_source", XContentHelper.convertToMap(source, false, sourceXContentType).v2());
8594
assert executedPipelines != null : "executedPipelines is null when it shouldn't be - we always list pipelines in simulate mode";
8695
builder.array("executed_pipelines", executedPipelines.toArray());
96+
if (ignoredFields.isEmpty() == false) {
97+
builder.startArray("ignored_fields");
98+
for (String ignoredField : ignoredFields) {
99+
builder.startObject();
100+
builder.field("field", ignoredField);
101+
builder.endObject();
102+
}
103+
;
104+
builder.endArray();
105+
}
87106
if (exception != null) {
88107
builder.startObject("error");
89108
ElasticsearchException.generateThrowableXContent(builder, params, exception);
@@ -105,6 +124,9 @@ public void writeTo(StreamOutput out) throws IOException {
105124
if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_15_0)) {
106125
out.writeException(exception);
107126
}
127+
if (out.getTransportVersion().onOrAfter(TransportVersions.SIMULATE_IGNORED_FIELDS)) {
128+
out.writeStringCollection(ignoredFields);
129+
}
108130
}
109131

110132
public Exception getException() {

0 commit comments

Comments
 (0)