Skip to content

Commit 658a75b

Browse files
committed
Merge dynamic field type lookup into FieldTypeLookup (#72024)
Flattened field mappers have a specialised lookup class to handle the fact that their MappedFieldTypes are dynamic and generated on-the-fly, rather than being registered up front. The way this is implemented means that this lookup class also has to be aware of field aliases, which is blocking our attempt to re-implement aliases as runtime fields. This commit merges dynamic field lookup handling into the standard FieldTypeLookup class. When the lookup class is built, it checks each MappedFieldType being registered to see if it implements a new DynamicFieldType interface, and stores these in a separate map. If a field containing dots does not have a field type directly registered against it, we check if a dynamic field type matches one of its dot- delimited prefixes, and if so we then return the result of calling `DynamicFieldType.getChildFieldType()` with the remainder of the path.
1 parent 6dc6f78 commit 658a75b

File tree

8 files changed

+158
-219
lines changed

8 files changed

+158
-219
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.index.mapper;
10+
11+
/**
12+
* Defines a MappedFieldType that exposes dynamic child field types
13+
*
14+
* If the field is named 'my_field', then a user is able to search on
15+
* the field in both of the following ways:
16+
* - Using the field name 'my_field', which will delegate to the field type
17+
* as usual.
18+
* - Using any sub-key, for example 'my_field.some_key'. In this case, the
19+
* search is delegated to {@link #getChildFieldType(String)}, with 'some_key'
20+
* passed as the argument. The field may create a new field type dynamically
21+
* in order to handle the search.
22+
*
23+
* To prevent conflicts between these dynamic sub-keys and multi-fields, any
24+
* field mappers generating field types that implement this interface should
25+
* explicitly disallow multi-fields.
26+
*/
27+
public interface DynamicFieldType {
28+
29+
/**
30+
* Returns a dynamic MappedFieldType for the given path
31+
*/
32+
MappedFieldType getChildFieldType(String path);
33+
34+
}

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

Lines changed: 0 additions & 41 deletions
This file was deleted.

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

Lines changed: 0 additions & 116 deletions
This file was deleted.

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

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
import java.util.HashMap;
1717
import java.util.HashSet;
1818
import java.util.Map;
19+
import java.util.Objects;
1920
import java.util.Set;
2021

2122
/**
2223
* An immutable container for looking up {@link MappedFieldType}s by their name.
2324
*/
2425
final class FieldTypeLookup {
2526
private final Map<String, MappedFieldType> fullNameToFieldType = new HashMap<>();
27+
private final Map<String, DynamicFieldType> dynamicFieldTypes = new HashMap<>();
2628

2729
/**
2830
* A map from field name to all fields whose content has been copied into it
@@ -33,7 +35,8 @@ final class FieldTypeLookup {
3335
*/
3436
private final Map<String, Set<String>> fieldToCopiedFields = new HashMap<>();
3537
private final String type;
36-
private final DynamicKeyFieldTypeLookup dynamicKeyLookup;
38+
39+
private final int maxParentPathDots;
3740

3841
FieldTypeLookup(
3942
String type,
@@ -42,16 +45,13 @@ final class FieldTypeLookup {
4245
Collection<RuntimeField> runtimeFields
4346
) {
4447
this.type = type;
45-
Map<String, DynamicKeyFieldMapper> dynamicKeyMappers = new HashMap<>();
46-
4748
for (FieldMapper fieldMapper : fieldMappers) {
4849
String fieldName = fieldMapper.name();
4950
MappedFieldType fieldType = fieldMapper.fieldType();
5051
fullNameToFieldType.put(fieldType.name(), fieldType);
51-
if (fieldMapper instanceof DynamicKeyFieldMapper) {
52-
dynamicKeyMappers.put(fieldName, (DynamicKeyFieldMapper) fieldMapper);
52+
if (fieldType instanceof DynamicFieldType) {
53+
dynamicFieldTypes.put(fieldType.name(), (DynamicFieldType) fieldType);
5354
}
54-
5555
for (String targetField : fieldMapper.copyTo().copyToFields()) {
5656
Set<String> sourcePath = fieldToCopiedFields.get(targetField);
5757
if (sourcePath == null) {
@@ -63,21 +63,37 @@ final class FieldTypeLookup {
6363
}
6464
}
6565

66-
final Map<String, String> aliasToConcreteName = new HashMap<>();
66+
int maxParentPathDots = 0;
67+
for (String dynamicRoot : dynamicFieldTypes.keySet()) {
68+
maxParentPathDots = Math.max(maxParentPathDots, dotCount(dynamicRoot));
69+
}
70+
this.maxParentPathDots = maxParentPathDots;
71+
6772
for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) {
6873
String aliasName = fieldAliasMapper.name();
6974
String path = fieldAliasMapper.path();
70-
aliasToConcreteName.put(aliasName, path);
71-
fullNameToFieldType.put(aliasName, fullNameToFieldType.get(path));
75+
MappedFieldType fieldType = fullNameToFieldType.get(path);
76+
fullNameToFieldType.put(aliasName, fieldType);
77+
if (fieldType instanceof DynamicFieldType) {
78+
dynamicFieldTypes.put(aliasName, (DynamicFieldType) fieldType);
79+
}
7280
}
7381

7482
for (RuntimeField runtimeField : runtimeFields) {
7583
MappedFieldType runtimeFieldType = runtimeField.asMappedFieldType();
7684
//this will override concrete fields with runtime fields that have the same name
7785
fullNameToFieldType.put(runtimeFieldType.name(), runtimeFieldType);
7886
}
87+
}
7988

80-
this.dynamicKeyLookup = new DynamicKeyFieldTypeLookup(dynamicKeyMappers, aliasToConcreteName);
89+
private static int dotCount(String path) {
90+
int dotCount = 0;
91+
for (int i = 0; i < path.length(); i++) {
92+
if (path.charAt(i) == '.') {
93+
dotCount++;
94+
}
95+
}
96+
return dotCount;
8197
}
8298

8399
/**
@@ -92,10 +108,42 @@ MappedFieldType get(String field) {
92108
if (fieldType != null) {
93109
return fieldType;
94110
}
111+
return getDynamicField(field);
112+
}
113+
114+
// for testing
115+
int getMaxParentPathDots() {
116+
return maxParentPathDots;
117+
}
118+
119+
// Check if the given field corresponds to a dynamic key mapper of the
120+
// form 'path_to_field.path_to_key'. If so, returns a field type that
121+
// can be used to perform searches on this field. Otherwise returns null.
122+
private MappedFieldType getDynamicField(String field) {
123+
if (dynamicFieldTypes.isEmpty()) {
124+
// no parent fields defined
125+
return null;
126+
}
127+
int dotIndex = -1;
128+
int fieldDepth = -1;
95129

96-
// If the mapping contains fields that support dynamic sub-key lookup, check
97-
// if this could correspond to a keyed field of the form 'path_to_field.path_to_key'.
98-
return dynamicKeyLookup.get(field);
130+
while (true) {
131+
if (++fieldDepth > maxParentPathDots) {
132+
return null;
133+
}
134+
135+
dotIndex = field.indexOf('.', dotIndex + 1);
136+
if (dotIndex < 0) {
137+
return null;
138+
}
139+
140+
String parentField = field.substring(0, dotIndex);
141+
DynamicFieldType dft = dynamicFieldTypes.get(parentField);
142+
if (dft != null && Objects.equals(field, parentField) == false) {
143+
String key = field.substring(dotIndex + 1);
144+
return dft.getChildFieldType(key);
145+
}
146+
}
99147
}
100148

101149
/**
@@ -138,7 +186,10 @@ Set<String> sourcePaths(String field) {
138186
if (fullNameToFieldType.isEmpty()) {
139187
return org.elasticsearch.common.collect.Set.of();
140188
}
141-
if (dynamicKeyLookup.get(field) != null) {
189+
190+
// If the field is dynamically generated then return its full path
191+
MappedFieldType fieldType = getDynamicField(field);
192+
if (fieldType != null) {
142193
return Collections.singleton(field);
143194
}
144195

server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import org.elasticsearch.index.fielddata.plain.AbstractLeafOrdinalsFieldData;
3333
import org.elasticsearch.index.fielddata.plain.SortedSetOrdinalsIndexFieldData;
3434
import org.elasticsearch.index.mapper.ContentPath;
35-
import org.elasticsearch.index.mapper.DynamicKeyFieldMapper;
35+
import org.elasticsearch.index.mapper.DynamicFieldType;
3636
import org.elasticsearch.index.mapper.FieldMapper;
3737
import org.elasticsearch.index.mapper.MappedFieldType;
3838
import org.elasticsearch.index.mapper.Mapper;
@@ -84,7 +84,7 @@
8484
* "key\0some value" and "key2.key3\0true". Note that \0 is used as a reserved separator
8585
* character (see {@link FlattenedFieldParser#SEPARATOR}).
8686
*/
87-
public final class FlattenedFieldMapper extends DynamicKeyFieldMapper {
87+
public final class FlattenedFieldMapper extends FieldMapper {
8888

8989
public static final String CONTENT_TYPE = "flattened";
9090
private static final String KEYED_FIELD_SUFFIX = "._keyed";
@@ -385,7 +385,7 @@ public IndexFieldData<?> build(IndexFieldDataCache cache, CircuitBreakerService
385385
* A field type that represents all 'root' values. This field type is used in
386386
* searches on the flattened field itself, e.g. 'my_flattened: some_value'.
387387
*/
388-
public static final class RootFlattenedFieldType extends StringFieldType {
388+
public static final class RootFlattenedFieldType extends StringFieldType implements DynamicFieldType {
389389
private final boolean splitQueriesOnWhitespace;
390390
private final boolean eagerGlobalOrdinals;
391391

@@ -426,6 +426,11 @@ public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, S
426426
public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
427427
return SourceValueFetcher.identity(name(), context, format);
428428
}
429+
430+
@Override
431+
public MappedFieldType getChildFieldType(String childPath) {
432+
return new KeyedFlattenedFieldType(name(), childPath, this);
433+
}
429434
}
430435

431436
private final FlattenedFieldParser fieldParser;
@@ -434,7 +439,7 @@ public ValueFetcher valueFetcher(SearchExecutionContext context, String format)
434439
private FlattenedFieldMapper(String simpleName,
435440
MappedFieldType mappedFieldType,
436441
Builder builder) {
437-
super(simpleName, mappedFieldType, Lucene.KEYWORD_ANALYZER, CopyTo.empty());
442+
super(simpleName, mappedFieldType, Lucene.KEYWORD_ANALYZER, MultiFields.empty(), CopyTo.empty());
438443
this.builder = builder;
439444
this.fieldParser = new FlattenedFieldParser(mappedFieldType.name(), mappedFieldType.name() + KEYED_FIELD_SUFFIX,
440445
mappedFieldType, builder.depthLimit.get(), builder.ignoreAbove.get(), builder.nullValue.get());
@@ -458,11 +463,6 @@ public RootFlattenedFieldType fieldType() {
458463
return (RootFlattenedFieldType) super.fieldType();
459464
}
460465

461-
@Override
462-
public KeyedFlattenedFieldType keyedFieldType(String key) {
463-
return new KeyedFlattenedFieldType(name(), key, fieldType());
464-
}
465-
466466
@Override
467467
protected void parseCreateField(ParseContext context) throws IOException {
468468
if (context.parser().currentToken() == XContentParser.Token.VALUE_NULL) {

0 commit comments

Comments
 (0)