Skip to content

Commit 9a6fcd6

Browse files
[8.x] Fix ignore_above for flattened fields (#112944) (#113308)
* Fix `ignore_above` for flattened fields (#112944) Flattened fields do no actually use `ignore_above`. When retrieving source, `ignore_above` behaves as expected. Anyway, it is ignored when fetching field values through the fields API. In that case values are returned no matter the `ignore_above` setting. Here we implement the filtering logic in the `ValueFecther` implementation of `RootFlattenedFieldType`. (cherry picked from commit 91674f8) * Fix --------- Co-authored-by: Salvatore Campagna <[email protected]>
1 parent afada29 commit 9a6fcd6

File tree

4 files changed

+289
-18
lines changed

4 files changed

+289
-18
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
flattened ignore_above single-value field:
3+
- requires:
4+
cluster_features: [ "flattened.ignore_above_support" ]
5+
reason: introduce ignore_above support in flattened fields
6+
- do:
7+
indices.create:
8+
index: test
9+
body:
10+
mappings:
11+
properties:
12+
keyword:
13+
type: keyword
14+
ignore_above: 5
15+
flat:
16+
type: flattened
17+
ignore_above: 5
18+
19+
- do:
20+
index:
21+
index: test
22+
id: "1"
23+
refresh: true
24+
body:
25+
keyword: "foo"
26+
flat: { "value": "foo", "key": "foo key" }
27+
28+
- do:
29+
index:
30+
index: test
31+
id: "2"
32+
refresh: true
33+
body:
34+
keyword: "foo bar"
35+
flat: { "value": "foo bar", "key": "foo bar key"}
36+
37+
- do:
38+
search:
39+
index: test
40+
body:
41+
fields:
42+
- keyword
43+
- flat
44+
query:
45+
match_all: {}
46+
47+
- match: { hits.total.value: 2 }
48+
49+
- match: { hits.hits.0._source.keyword: "foo" }
50+
- match: { hits.hits.0._source.flat.value: "foo" }
51+
- match: { hits.hits.0._source.flat.key: "foo key" }
52+
- match: { hits.hits.1._source.keyword: "foo bar" }
53+
- match: { hits.hits.1._source.flat.value: "foo bar" }
54+
- match: { hits.hits.1._source.flat.key: "foo bar key" }
55+
56+
- match: { hits.hits.0.fields.keyword.0: "foo" }
57+
- match: { hits.hits.0.fields.flat.0.value: "foo" }
58+
- match: { hits.hits.1.fields.keyword.0: null }
59+
- match: { hits.hits.1.fields.flat.0.value: null }
60+
61+
---
62+
flattened ignore_above multi-value field:
63+
- requires:
64+
cluster_features: [ "flattened.ignore_above_support" ]
65+
reason: introduce ignore_above support in flattened fields
66+
- do:
67+
indices.create:
68+
index: test
69+
body:
70+
mappings:
71+
properties:
72+
keyword:
73+
type: keyword
74+
ignore_above: 5
75+
flat:
76+
type: flattened
77+
ignore_above: 5
78+
79+
- do:
80+
index:
81+
index: test
82+
id: "1"
83+
refresh: true
84+
body:
85+
keyword: ["foo","bar"]
86+
flat: { "value": ["foo", "bar"], "key": "foo bar array key" }
87+
88+
- do:
89+
index:
90+
index: test
91+
id: "2"
92+
refresh: true
93+
body:
94+
keyword: ["foobar", "foo", "bar"]
95+
flat: { "value": ["foobar", "foo"], "key": ["foo key", "bar key"]}
96+
97+
- do:
98+
search:
99+
index: test
100+
body:
101+
fields:
102+
- keyword
103+
- flat
104+
query:
105+
match_all: { }
106+
107+
- match: { hits.total.value: 2 }
108+
109+
- match: { hits.hits.0._source.keyword: ["foo", "bar"] }
110+
- match: { hits.hits.0._source.flat.value: ["foo", "bar"] }
111+
- match: { hits.hits.0._source.flat.key: "foo bar array key" }
112+
- match: { hits.hits.1._source.keyword: ["foobar", "foo", "bar"] }
113+
- match: { hits.hits.1._source.flat.value: ["foobar", "foo"] }
114+
- match: { hits.hits.1._source.flat.key: ["foo key", "bar key"] }
115+
116+
- match: { hits.hits.0.fields.keyword: [ "foo", "bar" ] }
117+
- match: { hits.hits.0.fields.flat.0.value: ["foo", "bar"] }
118+
- match: { hits.hits.0.fields.flat.0.key: null }
119+
- match: { hits.hits.1.fields.keyword: [ "foo", "bar" ] }
120+
- match: { hits.hits.1.fields.flat.0.value: "foo" }
121+
- match: { hits.hits.1.fields.flat.0.key: null }

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import org.elasticsearch.features.FeatureSpecification;
1313
import org.elasticsearch.features.NodeFeature;
14+
import org.elasticsearch.index.mapper.flattened.FlattenedFieldMapper;
1415
import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper;
1516

1617
import java.util.Set;
@@ -38,7 +39,8 @@ public Set<NodeFeature> getFeatures() {
3839
SourceFieldMapper.SYNTHETIC_SOURCE_STORED_FIELDS_ADVANCE_FIX,
3940
Mapper.SYNTHETIC_SOURCE_KEEP_FEATURE,
4041
SourceFieldMapper.SYNTHETIC_SOURCE_WITH_COPY_TO_AND_DOC_VALUES_FALSE_SUPPORT,
41-
SourceFieldMapper.SYNTHETIC_SOURCE_COPY_TO_FIX
42+
SourceFieldMapper.SYNTHETIC_SOURCE_COPY_TO_FIX,
43+
FlattenedFieldMapper.IGNORE_ABOVE_SUPPORT
4244
);
4345
}
4446
}

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

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.elasticsearch.common.unit.Fuzziness;
3939
import org.elasticsearch.common.util.BigArrays;
4040
import org.elasticsearch.core.Nullable;
41+
import org.elasticsearch.features.NodeFeature;
4142
import org.elasticsearch.index.analysis.NamedAnalyzer;
4243
import org.elasticsearch.index.fielddata.FieldData;
4344
import org.elasticsearch.index.fielddata.FieldDataContext;
@@ -73,9 +74,12 @@
7374
import org.elasticsearch.xcontent.XContentParser;
7475

7576
import java.io.IOException;
77+
import java.util.ArrayList;
7678
import java.util.Collections;
79+
import java.util.HashMap;
7780
import java.util.List;
7881
import java.util.Map;
82+
import java.util.Set;
7983
import java.util.function.Function;
8084

8185
/**
@@ -105,6 +109,8 @@
105109
*/
106110
public final class FlattenedFieldMapper extends FieldMapper {
107111

112+
public static final NodeFeature IGNORE_ABOVE_SUPPORT = new NodeFeature("flattened.ignore_above_support");
113+
108114
public static final String CONTENT_TYPE = "flattened";
109115
public static final String KEYED_FIELD_SUFFIX = "._keyed";
110116
public static final String TIME_SERIES_DIMENSIONS_ARRAY_PARAM = "time_series_dimensions";
@@ -214,7 +220,8 @@ public FlattenedFieldMapper build(MapperBuilderContext context) {
214220
meta.get(),
215221
splitQueriesOnWhitespace.get(),
216222
eagerGlobalOrdinals.get(),
217-
dimensions.get()
223+
dimensions.get(),
224+
ignoreAbove.getValue()
218225
);
219226
return new FlattenedFieldMapper(leafName(), ft, builderParams(this, context), this);
220227
}
@@ -642,16 +649,18 @@ public static final class RootFlattenedFieldType extends StringFieldType impleme
642649
private final boolean eagerGlobalOrdinals;
643650
private final List<String> dimensions;
644651
private final boolean isDimension;
652+
private final int ignoreAbove;
645653

646654
public RootFlattenedFieldType(
647655
String name,
648656
boolean indexed,
649657
boolean hasDocValues,
650658
Map<String, String> meta,
651659
boolean splitQueriesOnWhitespace,
652-
boolean eagerGlobalOrdinals
660+
boolean eagerGlobalOrdinals,
661+
int ignoreAbove
653662
) {
654-
this(name, indexed, hasDocValues, meta, splitQueriesOnWhitespace, eagerGlobalOrdinals, Collections.emptyList());
663+
this(name, indexed, hasDocValues, meta, splitQueriesOnWhitespace, eagerGlobalOrdinals, Collections.emptyList(), ignoreAbove);
655664
}
656665

657666
public RootFlattenedFieldType(
@@ -661,7 +670,8 @@ public RootFlattenedFieldType(
661670
Map<String, String> meta,
662671
boolean splitQueriesOnWhitespace,
663672
boolean eagerGlobalOrdinals,
664-
List<String> dimensions
673+
List<String> dimensions,
674+
int ignoreAbove
665675
) {
666676
super(
667677
name,
@@ -675,6 +685,7 @@ public RootFlattenedFieldType(
675685
this.eagerGlobalOrdinals = eagerGlobalOrdinals;
676686
this.dimensions = dimensions;
677687
this.isDimension = dimensions.isEmpty() == false;
688+
this.ignoreAbove = ignoreAbove;
678689
}
679690

680691
@Override
@@ -708,7 +719,67 @@ public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext
708719

709720
@Override
710721
public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
711-
return SourceValueFetcher.identity(name(), context, format);
722+
if (format != null) {
723+
throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't support formats.");
724+
}
725+
return sourceValueFetcher(context.isSourceEnabled() ? context.sourcePath(name()) : Collections.emptySet());
726+
}
727+
728+
private SourceValueFetcher sourceValueFetcher(Set<String> sourcePaths) {
729+
return new SourceValueFetcher(sourcePaths, null) {
730+
@Override
731+
@SuppressWarnings("unchecked")
732+
protected Object parseSourceValue(Object value) {
733+
if (value instanceof Map<?, ?> valueAsMap && valueAsMap.isEmpty() == false) {
734+
final Map<String, Object> result = filterIgnoredValues((Map<String, Object>) valueAsMap);
735+
return result.isEmpty() ? null : result;
736+
}
737+
if (value instanceof String valueAsString && valueAsString.length() <= ignoreAbove) {
738+
return valueAsString;
739+
}
740+
return null;
741+
}
742+
743+
private Map<String, Object> filterIgnoredValues(final Map<String, Object> values) {
744+
final Map<String, Object> result = new HashMap<>();
745+
for (final Map.Entry<String, Object> entry : values.entrySet()) {
746+
Object value = filterIgnoredValues(entry.getValue());
747+
if (value != null) {
748+
result.put(entry.getKey(), value);
749+
}
750+
}
751+
return result;
752+
}
753+
754+
private Object filterIgnoredValues(final Object entryValue) {
755+
if (entryValue instanceof List<?> valueAsList) {
756+
final List<Object> validValues = new ArrayList<>();
757+
for (Object value : valueAsList) {
758+
if (value instanceof String valueAsString) {
759+
if (valueAsString.length() <= ignoreAbove) {
760+
validValues.add(valueAsString);
761+
}
762+
} else {
763+
validValues.add(value);
764+
}
765+
}
766+
if (validValues.isEmpty()) {
767+
return null;
768+
}
769+
if (validValues.size() == 1) {
770+
// NOTE: for single-value flattened fields do not return an array
771+
return validValues.get(0);
772+
}
773+
return validValues;
774+
} else if (entryValue instanceof String valueAsString) {
775+
if (valueAsString.length() <= ignoreAbove) {
776+
return valueAsString;
777+
}
778+
return null;
779+
}
780+
return entryValue;
781+
}
782+
};
712783
}
713784

714785
@Override

0 commit comments

Comments
 (0)