Skip to content

Commit 417f3bb

Browse files
authored
Parse the contents of dynamic objects for [subobjects:false] (#117762) (#117921)
* Parse the contents of dynamic objects for [subobjects:false] * Update docs/changelog/117762.yaml * add tests * tests * test dynamic field * test dynamic field * fix tests (cherry picked from commit f2addbc) # Conflicts: # server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
1 parent fdade16 commit 417f3bb

File tree

5 files changed

+194
-1
lines changed

5 files changed

+194
-1
lines changed

docs/changelog/117762.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 117762
2+
summary: "Parse the contents of dynamic objects for [subobjects:false]"
3+
area: Mapping
4+
type: bug
5+
issues:
6+
- 117544

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/330_fetch_fields.yml

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,3 +1177,121 @@ fetch geo_point:
11771177
- is_false: hits.hits.0.fields.message
11781178
- match: { hits.hits.0._source.message.foo: 10 }
11791179
- match: { hits.hits.0._source.message.foo\.bar: 20 }
1180+
1181+
---
1182+
root with subobjects false and dynamic false:
1183+
- requires:
1184+
cluster_features: mapper.fix_parsing_subobjects_false_dynamic_false
1185+
reason: bug fix
1186+
1187+
- do:
1188+
indices.create:
1189+
index: test
1190+
body:
1191+
mappings:
1192+
subobjects: false
1193+
dynamic: false
1194+
properties:
1195+
id:
1196+
type: integer
1197+
my.keyword.field:
1198+
type: keyword
1199+
1200+
- do:
1201+
bulk:
1202+
index: test
1203+
refresh: true
1204+
body:
1205+
- '{ "index": { } }'
1206+
- '{ "id": 1, "my": { "keyword.field": "abc" } }'
1207+
- match: { errors: false }
1208+
1209+
# indexing a dynamically-mapped field still fails (silently)
1210+
- do:
1211+
bulk:
1212+
index: test
1213+
refresh: true
1214+
body:
1215+
- '{ "index": { } }'
1216+
- '{ "id": 2, "my": { "random.field": "abc" } }'
1217+
- match: { errors: false }
1218+
1219+
- do:
1220+
search:
1221+
index: test
1222+
body:
1223+
sort: id
1224+
fields: [ "*" ]
1225+
1226+
- match: { hits.hits.0.fields: { my.keyword.field: [ abc ], id: [ 1 ] } }
1227+
- match: { hits.hits.1.fields: { id: [ 2 ] } }
1228+
1229+
- do:
1230+
search:
1231+
index: test
1232+
body:
1233+
query:
1234+
match:
1235+
my.keyword.field: abc
1236+
1237+
- match: { hits.total.value: 1 }
1238+
1239+
---
1240+
object with subobjects false and dynamic false:
1241+
- requires:
1242+
cluster_features: mapper.fix_parsing_subobjects_false_dynamic_false
1243+
reason: bug fix
1244+
1245+
- do:
1246+
indices.create:
1247+
index: test
1248+
body:
1249+
mappings:
1250+
properties:
1251+
my:
1252+
subobjects: false
1253+
dynamic: false
1254+
properties:
1255+
id:
1256+
type: integer
1257+
nested.keyword.field:
1258+
type: keyword
1259+
1260+
- do:
1261+
bulk:
1262+
index: test
1263+
refresh: true
1264+
body:
1265+
- '{ "index": { } }'
1266+
- '{ "id": 1, "my": { "nested": { "keyword.field": "abc" } } }'
1267+
- match: { errors: false }
1268+
1269+
# indexing a dynamically-mapped field still fails (silently)
1270+
- do:
1271+
bulk:
1272+
index: test
1273+
refresh: true
1274+
body:
1275+
- '{ "index": { } }'
1276+
- '{ "id": 2, "my": { "nested": { "random.field": "abc" } } }'
1277+
- match: { errors: false }
1278+
1279+
- do:
1280+
search:
1281+
index: test
1282+
body:
1283+
sort: id
1284+
fields: [ "*" ]
1285+
1286+
- match: { hits.hits.0.fields: { my.nested.keyword.field: [ abc ], id: [ 1 ] } }
1287+
- match: { hits.hits.1.fields: { id: [ 2 ] } }
1288+
1289+
- do:
1290+
search:
1291+
index: test
1292+
body:
1293+
query:
1294+
match:
1295+
my.nested.keyword.field: abc
1296+
1297+
- match: { hits.total.value: 1 }

server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.common.regex.Regex;
1717
import org.elasticsearch.common.xcontent.XContentHelper;
1818
import org.elasticsearch.core.Nullable;
19+
import org.elasticsearch.features.NodeFeature;
1920
import org.elasticsearch.index.IndexVersion;
2021
import org.elasticsearch.index.IndexVersions;
2122
import org.elasticsearch.index.fielddata.FieldDataContext;
@@ -53,6 +54,9 @@
5354
public final class DocumentParser {
5455

5556
public static final IndexVersion DYNAMICALLY_MAP_DENSE_VECTORS_INDEX_VERSION = IndexVersions.FIRST_DETACHED_INDEX_VERSION;
57+
static final NodeFeature FIX_PARSING_SUBOBJECTS_FALSE_DYNAMIC_FALSE = new NodeFeature(
58+
"mapper.fix_parsing_subobjects_false_dynamic_false"
59+
);
5660

5761
private final XContentParserConfiguration parserConfiguration;
5862
private final MappingParserContext mappingParserContext;
@@ -531,7 +535,8 @@ private static void doParseObject(DocumentParserContext context, String currentF
531535

532536
private static void parseObjectDynamic(DocumentParserContext context, String currentFieldName) throws IOException {
533537
ensureNotStrict(context, currentFieldName);
534-
if (context.dynamic() == ObjectMapper.Dynamic.FALSE) {
538+
// For [subobjects:false], intermediate objects get flattened so we can't skip parsing children.
539+
if (context.dynamic() == ObjectMapper.Dynamic.FALSE && context.parent().subobjects() != ObjectMapper.Subobjects.DISABLED) {
535540
failIfMatchesRoutingPath(context, currentFieldName);
536541
if (context.canAddIgnoredField()) {
537542
context.addIgnoredField(

server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public Set<NodeFeature> getTestFeatures() {
6464
SourceFieldMapper.SOURCE_MODE_FROM_INDEX_SETTING,
6565
IgnoredSourceFieldMapper.ALWAYS_STORE_OBJECT_ARRAYS_IN_NESTED_OBJECTS,
6666
MapperService.LOGSDB_DEFAULT_IGNORE_DYNAMIC_BEYOND_LIMIT,
67+
DocumentParser.FIX_PARSING_SUBOBJECTS_FALSE_DYNAMIC_FALSE,
6768
CONSTANT_KEYWORD_SYNTHETIC_SOURCE_WRITE_FIX
6869
);
6970
}

server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,6 +2053,38 @@ public void testSubobjectsFalseWithInnerDottedObject() throws Exception {
20532053
assertNotNull(doc.rootDoc().getField("metrics.service.test.with.dots.max"));
20542054
}
20552055

2056+
public void testSubobjectsFalseWithInnerDottedObjectDynamicFalse() throws Exception {
2057+
DocumentMapper mapper = createDocumentMapper(mapping(b -> {
2058+
b.startObject("metrics").field("type", "object").field("subobjects", false).field("dynamic", randomFrom("false", "runtime"));
2059+
b.startObject("properties").startObject("service.test.with.dots").field("type", "keyword").endObject().endObject();
2060+
b.endObject();
2061+
}));
2062+
2063+
ParsedDocument doc = mapper.parse(source("""
2064+
{ "metrics": { "service": { "test.with.dots": "foo" } } }"""));
2065+
assertNotNull(doc.rootDoc().getField("metrics.service.test.with.dots"));
2066+
2067+
doc = mapper.parse(source("""
2068+
{ "metrics": { "service.test": { "with.dots": "foo" } } }"""));
2069+
assertNotNull(doc.rootDoc().getField("metrics.service.test.with.dots"));
2070+
2071+
doc = mapper.parse(source("""
2072+
{ "metrics": { "service": { "test": { "with.dots": "foo" } } } }"""));
2073+
assertNotNull(doc.rootDoc().getField("metrics.service.test.with.dots"));
2074+
2075+
doc = mapper.parse(source("""
2076+
{ "metrics": { "service": { "test.other.dots": "foo" } } }"""));
2077+
assertNull(doc.rootDoc().getField("metrics.service.test.other.dots"));
2078+
2079+
doc = mapper.parse(source("""
2080+
{ "metrics": { "service.test": { "other.dots": "foo" } } }"""));
2081+
assertNull(doc.rootDoc().getField("metrics.service.test.other.dots"));
2082+
2083+
doc = mapper.parse(source("""
2084+
{ "metrics": { "service": { "test": { "other.dots": "foo" } } } }"""));
2085+
assertNull(doc.rootDoc().getField("metrics.service.test.other.dots"));
2086+
}
2087+
20562088
public void testSubobjectsFalseRoot() throws Exception {
20572089
DocumentMapper mapper = createDocumentMapper(mappingNoSubobjects(xContentBuilder -> {}));
20582090
ParsedDocument doc = mapper.parse(source("""
@@ -2074,6 +2106,37 @@ public void testSubobjectsFalseRoot() throws Exception {
20742106
assertNotNull(doc.rootDoc().getField("metrics.service.test.with.dots"));
20752107
}
20762108

2109+
public void testSubobjectsFalseRootWithInnerDottedObjectDynamicFalse() throws Exception {
2110+
DocumentMapper mapper = createDocumentMapper(topMapping(b -> {
2111+
b.field("subobjects", false).field("dynamic", randomFrom("false", "runtime"));
2112+
b.startObject("properties").startObject("service.test.with.dots").field("type", "keyword").endObject().endObject();
2113+
}));
2114+
2115+
ParsedDocument doc = mapper.parse(source("""
2116+
{ "service": { "test.with.dots": "foo" } }"""));
2117+
assertNotNull(doc.rootDoc().getField("service.test.with.dots"));
2118+
2119+
doc = mapper.parse(source("""
2120+
{ "service.test": { "with.dots": "foo" } }"""));
2121+
assertNotNull(doc.rootDoc().getField("service.test.with.dots"));
2122+
2123+
doc = mapper.parse(source("""
2124+
{ "service": { "test": { "with.dots": "foo" } } }"""));
2125+
assertNotNull(doc.rootDoc().getField("service.test.with.dots"));
2126+
2127+
doc = mapper.parse(source("""
2128+
{ "service": { "test.other.dots": "foo" } }"""));
2129+
assertNull(doc.rootDoc().getField("service.test.other.dots"));
2130+
2131+
doc = mapper.parse(source("""
2132+
{ "service.test": { "other.dots": "foo" } }"""));
2133+
assertNull(doc.rootDoc().getField("service.test.other.dots"));
2134+
2135+
doc = mapper.parse(source("""
2136+
{ "service": { "test": { "other.dots": "foo" } } }"""));
2137+
assertNull(doc.rootDoc().getField("service.test.other.dots"));
2138+
}
2139+
20772140
public void testSubobjectsFalseStructuredPath() throws Exception {
20782141
DocumentMapper mapper = createDocumentMapper(
20792142
mapping(b -> b.startObject("metrics.service").field("type", "object").field("subobjects", false).endObject())

0 commit comments

Comments
 (0)