Skip to content

Commit b0980f7

Browse files
committed
Do not exclude empty arrays in source filtering with Jackson streaming
Fixes elastic#109668. This fixes the specific case outlined in the above linked Github issue - namely this: * Document: `{ "myArray": [] }` * Source filter: `myArray.myField` * Expected filtered result: `{ "myArray": [] }` * Actual filtered result: `{}` When we switched to from map-based source filtering to Jackson-streaming-based source filtering in some code paths as part of filtering (using the automata, etc.) correctly included the empty array from the parent. Unfortunately, it's not clear when these two code paths for source filtering diverge in behavior. Note also the long comment in `FilterPathBasedFilter`. It's not clear what the correct behavior is when we are filtering out all contents of arrays via source filter path exclusion.
1 parent ffc22b2 commit b0980f7

File tree

2 files changed

+45
-0
lines changed

2 files changed

+45
-0
lines changed

libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/filtering/FilterPathBasedFilter.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,31 @@ public TokenFilter includeProperty(String name) {
9595
return filter;
9696
}
9797

98+
/**
99+
* This is overridden in order to keep empty arrays in nested exclusions - see #109668.
100+
* <p>
101+
* If we are excluding contents, we only want to exclude based on property name - but empty arrays in themselves do not have a property
102+
* name. If the empty array were to be excluded, it should be done by excluding the parent.
103+
* <p>
104+
* Note though that the expected behavior seems to be ambiguous if contentsFiltered is true - that is, that the filter has pruned all
105+
* the contents of a given array, such that we are left with the empty array. The behavior below drops that array, for at the time of
106+
* writing, not doing so would cause assertions in JsonXContentFilteringTests to fail, which expect this behavior. Yet it is not obvious
107+
* if dropping the empty array in this case is correct. For example, one could expect this sort of behavior:
108+
* <ul>
109+
* <li>Document: <pre>{ "myArray": [ { "myField": "myValue" } ]}</pre></li>
110+
* <li>Filter: <pre>{ "exclude": "myArray.myField" }</pre></li>
111+
* </ul>
112+
* From the user's perspective, this could reasonably yield either of:
113+
* <ol>
114+
* <li><pre>{ "myArray": []}</pre></li>
115+
* <li>Removing {@code myArray} entirely.</li>
116+
* </ol>
117+
*/
118+
@Override
119+
public boolean includeEmptyArray(boolean contentsFiltered) {
120+
return !inclusive && !contentsFiltered;
121+
}
122+
98123
@Override
99124
protected boolean _includeScalar() {
100125
return inclusive == false;

server/src/test/java/org/elasticsearch/search/lookup/SourceFilterTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.test.ESTestCase;
1414
import org.elasticsearch.xcontent.XContentType;
1515

16+
import java.util.Arrays;
1617
import java.util.List;
1718
import java.util.Map;
1819

@@ -111,4 +112,23 @@ public Source filter(SourceFilter sourceFilter) {
111112

112113
}
113114

115+
// Verification for issue #109668
116+
public void testIncludeParentAndExcludeChildEmptyArray() {
117+
Source fromMap = Source.fromMap(Map.of("myArray", List.of()), XContentType.JSON);
118+
Source filteredMap = fromMap.filter(new SourceFilter(new String[] {"myArray"}, new String[]{"myArray.myField"}));
119+
assertEquals(filteredMap.source(), Map.of("myArray", List.of()));
120+
Source fromBytes = Source.fromBytes(new BytesArray("{\"myArray\": []}"), XContentType.JSON);
121+
Source filteredBytes = fromBytes.filter(new SourceFilter(new String[] {"myArray"}, new String[]{"myArray.myField"}));
122+
assertEquals(filteredBytes.source(), Map.of("myArray", List.of()));
123+
}
124+
125+
public void testIncludeParentAndExcludeChildSubFields() {
126+
Source fromMap = Source.fromMap(Map.of("myArray", List.of(Map.<String, Object>of("myField", "myValue", "other", "otherValue"))), XContentType.JSON);
127+
Source filteredMap = fromMap.filter(new SourceFilter(new String[] {"myArray"}, new String[]{"myArray.myField"}));
128+
assertEquals(filteredMap.source(), Map.of("myArray", List.of(Map.of("other", "otherValue"))));
129+
Source fromBytes = Source.fromBytes(new BytesArray("""
130+
{ "myArray": [ { "myField": "myValue", "other": "otherValue" } ] }"""), XContentType.JSON);
131+
Source filteredBytes = fromBytes.filter(new SourceFilter(new String[] {"myArray"}, new String[]{"myArray.myField"}));
132+
assertEquals(filteredBytes.source(), Map.of("myArray", List.of(Map.of("other", "otherValue"))));
133+
}
114134
}

0 commit comments

Comments
 (0)