Skip to content

Commit f7d378c

Browse files
committed
Prevent slow field lookups when JSON fields are present. (#39872)
We now track the maximum depth of any JSON field, which allows the JSON field lookup to be short-circuited as soon as that depth is reached. This helps prevent slow lookups when the user is searching over a very deep field that is not in the mappings.
1 parent 06f6081 commit f7d378c

File tree

4 files changed

+96
-7
lines changed

4 files changed

+96
-7
lines changed

rest-api-spec/src/main/resources/rest-api-spec/test/search/160_exists_query.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,8 +1332,8 @@ setup:
13321332
---
13331333
"Test exists query on JSON field":
13341334
- skip:
1335-
version: " - 6.99.99"
1336-
reason: "JSON fields are currently only implemented in 7.0."
1335+
version: " - 7.99.99"
1336+
reason: "JSON fields are currently only implemented in 8.0."
13371337

13381338
- do:
13391339
indices.create:

rest-api-spec/src/main/resources/rest-api-spec/test/search/60_query_string.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@
6565
---
6666
"search on JSON field":
6767
- skip:
68-
version: " - 6.99.99"
69-
reason: "JSON fields are currently only implemented in 7.0."
68+
version: " - 7.99.99"
69+
reason: "JSON fields are currently only implemented in 8.0."
7070

7171
- do:
7272
indices.create:

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

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Collection;
2626
import java.util.HashSet;
2727
import java.util.Iterator;
28+
import java.util.Map;
2829
import java.util.Objects;
2930
import java.util.Set;
3031

@@ -35,20 +36,25 @@ class FieldTypeLookup implements Iterable<MappedFieldType> {
3536

3637
final CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType;
3738
private final CopyOnWriteHashMap<String, String> aliasToConcreteName;
39+
3840
private final CopyOnWriteHashMap<String, JsonFieldMapper> fullNameToJsonMapper;
41+
private final int maxJsonFieldDepth;
3942

4043
FieldTypeLookup() {
4144
fullNameToFieldType = new CopyOnWriteHashMap<>();
4245
aliasToConcreteName = new CopyOnWriteHashMap<>();
4346
fullNameToJsonMapper = new CopyOnWriteHashMap<>();
47+
maxJsonFieldDepth = 0;
4448
}
4549

4650
private FieldTypeLookup(CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType,
4751
CopyOnWriteHashMap<String, String> aliasToConcreteName,
48-
CopyOnWriteHashMap<String, JsonFieldMapper> fullNameToJsonMapper) {
52+
CopyOnWriteHashMap<String, JsonFieldMapper> fullNameToJsonMapper,
53+
int maxJsonFieldDepth) {
4954
this.fullNameToFieldType = fullNameToFieldType;
5055
this.aliasToConcreteName = aliasToConcreteName;
5156
this.fullNameToJsonMapper = fullNameToJsonMapper;
57+
this.maxJsonFieldDepth = maxJsonFieldDepth;
5258
}
5359

5460
/**
@@ -70,6 +76,7 @@ public FieldTypeLookup copyAndAddAll(String type,
7076
CopyOnWriteHashMap<String, JsonFieldMapper> jsonMappers = this.fullNameToJsonMapper;
7177

7278
for (FieldMapper fieldMapper : fieldMappers) {
79+
String fieldName = fieldMapper.name();
7380
MappedFieldType fieldType = fieldMapper.fieldType();
7481
MappedFieldType fullNameFieldType = fullName.get(fieldType.name());
7582

@@ -78,7 +85,7 @@ public FieldTypeLookup copyAndAddAll(String type,
7885
}
7986

8087
if (fieldMapper instanceof JsonFieldMapper) {
81-
jsonMappers = fullNameToJsonMapper.copyAndPut(fieldType.name(), (JsonFieldMapper) fieldMapper);
88+
jsonMappers = fullNameToJsonMapper.copyAndPut(fieldName, (JsonFieldMapper) fieldMapper);
8289
}
8390
}
8491

@@ -92,7 +99,43 @@ public FieldTypeLookup copyAndAddAll(String type,
9299
}
93100
}
94101

95-
return new FieldTypeLookup(fullName, aliases, jsonMappers);
102+
int maxFieldDepth = getMaxJsonFieldDepth(aliases, jsonMappers);
103+
104+
return new FieldTypeLookup(fullName, aliases, jsonMappers, maxFieldDepth);
105+
}
106+
107+
private static int getMaxJsonFieldDepth(CopyOnWriteHashMap<String, String> aliases,
108+
CopyOnWriteHashMap<String, JsonFieldMapper> jsonMappers) {
109+
int maxFieldDepth = 0;
110+
for (Map.Entry<String, String> entry : aliases.entrySet()) {
111+
String aliasName = entry.getKey();
112+
String path = entry.getValue();
113+
if (jsonMappers.containsKey(path)) {
114+
maxFieldDepth = Math.max(maxFieldDepth, fieldDepth(aliasName));
115+
}
116+
}
117+
118+
for (String fieldName : jsonMappers.keySet()) {
119+
if (jsonMappers.containsKey(fieldName)) {
120+
maxFieldDepth = Math.max(maxFieldDepth, fieldDepth(fieldName));
121+
}
122+
}
123+
124+
return maxFieldDepth;
125+
}
126+
127+
/**
128+
* Computes the total depth of this field by counting the number of parent fields
129+
* in its path. As an example, the field 'parent1.parent2.field' has depth 3.
130+
*/
131+
private static int fieldDepth(String field) {
132+
int numDots = 0;
133+
for (int i = 0; i < field.length(); ++i) {
134+
if (field.charAt(i) == '.') {
135+
numDots++;
136+
}
137+
}
138+
return numDots + 1;
96139
}
97140

98141

@@ -111,9 +154,20 @@ public MappedFieldType get(String field) {
111154
return !fullNameToJsonMapper.isEmpty() ? getKeyedJsonField(field) : null;
112155
}
113156

157+
/**
158+
* Check if the given field corresponds to a keyed JSON field of the form
159+
* 'path_to_json_field.path_to_key'. If so, returns a field type that can
160+
* be used to perform searches on this field.
161+
*/
114162
private MappedFieldType getKeyedJsonField(String field) {
115163
int dotIndex = -1;
164+
int fieldDepth = 0;
165+
116166
while (true) {
167+
if (++fieldDepth > maxJsonFieldDepth) {
168+
return null;
169+
}
170+
117171
dotIndex = field.indexOf('.', dotIndex + 1);
118172
if (dotIndex < 0) {
119173
return null;
@@ -152,4 +206,9 @@ public Collection<String> simpleMatchToFullName(String pattern) {
152206
public Iterator<MappedFieldType> iterator() {
153207
return fullNameToFieldType.values().iterator();
154208
}
209+
210+
// Visible for testing.
211+
int maxJsonFieldDepth() {
212+
return maxJsonFieldDepth;
213+
}
155214
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,36 @@ public void testJsonFieldTypeWithAlias() {
186186
assertEquals(objectKey, keyedFieldType.key());
187187
}
188188

189+
public void testMaxJsonFieldDepth() {
190+
FieldTypeLookup lookup = new FieldTypeLookup();
191+
assertEquals(0, lookup.maxJsonFieldDepth());
192+
193+
// Add a JSON field.
194+
String jsonFieldName = "object1.object2.field";
195+
JsonFieldMapper jsonField = createJsonMapper(jsonFieldName);
196+
lookup = lookup.copyAndAddAll("type", newList(jsonField), emptyList());
197+
assertEquals(3, lookup.maxJsonFieldDepth());
198+
199+
// Add a short alias to that field.
200+
String aliasName = "alias";
201+
FieldAliasMapper alias = new FieldAliasMapper(aliasName, aliasName, jsonFieldName);
202+
lookup = lookup.copyAndAddAll("type", emptyList(), newList(alias));
203+
assertEquals(3, lookup.maxJsonFieldDepth());
204+
205+
// Add a longer alias to that field.
206+
String longAliasName = "object1.object2.object3.alias";
207+
FieldAliasMapper longAlias = new FieldAliasMapper(longAliasName, longAliasName, jsonFieldName);
208+
lookup = lookup.copyAndAddAll("type", emptyList(), newList(longAlias));
209+
assertEquals(4, lookup.maxJsonFieldDepth());
210+
211+
// Update the long alias to refer to a non-JSON field.
212+
String fieldName = "field";
213+
MockFieldMapper field = new MockFieldMapper(fieldName);
214+
longAlias = new FieldAliasMapper(longAliasName, longAliasName, fieldName);
215+
lookup = lookup.copyAndAddAll("type", newList(field), newList(longAlias));
216+
assertEquals(3, lookup.maxJsonFieldDepth());
217+
}
218+
189219
private JsonFieldMapper createJsonMapper(String fieldName) {
190220
Settings settings = Settings.builder()
191221
.put("index.version.created", Version.CURRENT)

0 commit comments

Comments
 (0)