Skip to content

Commit aa29a57

Browse files
authored
Add Optional Source Filtering to Source Loaders (#113827) (#118459)
This change introduces optional source filtering directly within source loaders (both synthetic and stored). The main benefit is seen in synthetic source loaders, as synthetic fields are stored independently. By filtering while loading the synthetic source, generating the source becomes linear in the number of fields that match the filter. This update also modifies the get document API to apply source filters earlier—directly through the source loader. The search API, however, is not affected in this change, since the loaded source is still used by other features (e.g., highlighting, fields, nested hits), and source filtering is always applied as the final step. A follow-up will be required to ensure careful handling of all search-related scenarios.
1 parent 613a7aa commit aa29a57

File tree

41 files changed

+736
-166
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+736
-166
lines changed

benchmarks/src/main/java/org/elasticsearch/benchmark/search/fetch/subphase/FetchSourcePhaseBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public void setup() throws IOException {
6363
);
6464
includesSet = Set.of(fetchContext.includes());
6565
excludesSet = Set.of(fetchContext.excludes());
66-
parserConfig = XContentParserConfiguration.EMPTY.withFiltering(includesSet, excludesSet, false);
66+
parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, includesSet, excludesSet, false);
6767
}
6868

6969
private BytesReference read300BytesExample() throws IOException {

benchmarks/src/main/java/org/elasticsearch/benchmark/xcontent/FilterContentBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ private XContentParserConfiguration buildParseConfig(boolean matchDotsInFieldNam
170170
includes = null;
171171
excludes = filters;
172172
}
173-
return XContentParserConfiguration.EMPTY.withFiltering(includes, excludes, matchDotsInFieldNames);
173+
return XContentParserConfiguration.EMPTY.withFiltering(null, includes, excludes, matchDotsInFieldNames);
174174
}
175175

176176
private BytesReference filter(XContentParserConfiguration contentParserConfiguration) throws IOException {

docs/changelog/113827.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 113827
2+
summary: Add Optional Source Filtering to Source Loaders
3+
area: Mapping
4+
type: enhancement
5+
issues: []

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.elasticsearch.xcontent.provider.filtering.FilterPathBasedFilter;
2020
import org.elasticsearch.xcontent.support.filtering.FilterPath;
2121

22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import java.util.Set;
2325

2426
public class XContentParserConfigurationImpl implements XContentParserConfiguration {
@@ -106,12 +108,41 @@ public XContentParserConfiguration withFiltering(
106108
Set<String> excludeStrings,
107109
boolean filtersMatchFieldNamesWithDots
108110
) {
111+
return withFiltering(null, includeStrings, excludeStrings, filtersMatchFieldNamesWithDots);
112+
}
113+
114+
public XContentParserConfiguration withFiltering(
115+
String prefixPath,
116+
Set<String> includeStrings,
117+
Set<String> excludeStrings,
118+
boolean filtersMatchFieldNamesWithDots
119+
) {
120+
FilterPath[] includePaths = FilterPath.compile(includeStrings);
121+
FilterPath[] excludePaths = FilterPath.compile(excludeStrings);
122+
123+
if (prefixPath != null) {
124+
if (includePaths != null) {
125+
List<FilterPath> includeFilters = new ArrayList<>();
126+
for (var incl : includePaths) {
127+
incl.matches(prefixPath, includeFilters, true);
128+
}
129+
includePaths = includeFilters.isEmpty() ? null : includeFilters.toArray(FilterPath[]::new);
130+
}
131+
132+
if (excludePaths != null) {
133+
List<FilterPath> excludeFilters = new ArrayList<>();
134+
for (var excl : excludePaths) {
135+
excl.matches(prefixPath, excludeFilters, true);
136+
}
137+
excludePaths = excludeFilters.isEmpty() ? null : excludeFilters.toArray(FilterPath[]::new);
138+
}
139+
}
109140
return new XContentParserConfigurationImpl(
110141
registry,
111142
deprecationHandler,
112143
restApiVersion,
113-
FilterPath.compile(includeStrings),
114-
FilterPath.compile(excludeStrings),
144+
includePaths,
145+
excludePaths,
115146
filtersMatchFieldNamesWithDots
116147
);
117148
}

libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentParserConfiguration.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,27 @@ public interface XContentParserConfiguration {
4949

5050
RestApiVersion restApiVersion();
5151

52+
// TODO: Remove when serverless uses the new API
53+
XContentParserConfiguration withFiltering(
54+
Set<String> includeStrings,
55+
Set<String> excludeStrings,
56+
boolean filtersMatchFieldNamesWithDots
57+
);
58+
5259
/**
5360
* Replace the configured filtering.
61+
*
62+
* @param prefixPath The path to be prepended to each sub-path before applying the include/exclude rules.
63+
* Specify {@code null} if parsing starts from the root.
64+
* @param includeStrings A set of strings representing paths to include during filtering.
65+
* If specified, only these paths will be included in parsing.
66+
* @param excludeStrings A set of strings representing paths to exclude during filtering.
67+
* If specified, these paths will be excluded from parsing.
68+
* @param filtersMatchFieldNamesWithDots Indicates whether filters should match field names containing dots ('.')
69+
* as part of the field name.
5470
*/
5571
XContentParserConfiguration withFiltering(
72+
String prefixPath,
5673
Set<String> includeStrings,
5774
Set<String> excludeStrings,
5875
boolean filtersMatchFieldNamesWithDots

libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.IOException;
2323
import java.util.Arrays;
2424
import java.util.Collection;
25+
import java.util.HashSet;
2526
import java.util.Set;
2627
import java.util.stream.IntStream;
2728

@@ -332,6 +333,24 @@ protected final void testFilter(Builder expected, Builder sample, Collection<Str
332333
private void testFilter(Builder expected, Builder sample, Set<String> includes, Set<String> excludes, boolean matchFieldNamesWithDots)
333334
throws IOException {
334335
assertFilterResult(expected.apply(createBuilder()), filter(sample, includes, excludes, matchFieldNamesWithDots));
336+
337+
String rootPrefix = "root.path.random";
338+
if (includes != null) {
339+
Set<String> rootIncludes = new HashSet<>();
340+
for (var incl : includes) {
341+
rootIncludes.add(rootPrefix + (randomBoolean() ? "." : "*.") + incl);
342+
}
343+
includes = rootIncludes;
344+
}
345+
346+
if (excludes != null) {
347+
Set<String> rootExcludes = new HashSet<>();
348+
for (var excl : excludes) {
349+
rootExcludes.add(rootPrefix + (randomBoolean() ? "." : "*.") + excl);
350+
}
351+
excludes = rootExcludes;
352+
}
353+
assertFilterResult(expected.apply(createBuilder()), filterSub(sample, rootPrefix, includes, excludes, matchFieldNamesWithDots));
335354
}
336355

337356
public void testArrayWithEmptyObjectInInclude() throws IOException {
@@ -413,21 +432,36 @@ private XContentBuilder filter(Builder sample, Set<String> includes, Set<String>
413432
&& matchFieldNamesWithDots == false) {
414433
return filterOnBuilder(sample, includes, excludes);
415434
}
416-
return filterOnParser(sample, includes, excludes, matchFieldNamesWithDots);
435+
return filterOnParser(sample, null, includes, excludes, matchFieldNamesWithDots);
436+
}
437+
438+
private XContentBuilder filterSub(
439+
Builder sample,
440+
String root,
441+
Set<String> includes,
442+
Set<String> excludes,
443+
boolean matchFieldNamesWithDots
444+
) throws IOException {
445+
return filterOnParser(sample, root, includes, excludes, matchFieldNamesWithDots);
417446
}
418447

419448
private XContentBuilder filterOnBuilder(Builder sample, Set<String> includes, Set<String> excludes) throws IOException {
420449
return sample.apply(XContentBuilder.builder(getXContentType(), includes, excludes));
421450
}
422451

423-
private XContentBuilder filterOnParser(Builder sample, Set<String> includes, Set<String> excludes, boolean matchFieldNamesWithDots)
424-
throws IOException {
452+
private XContentBuilder filterOnParser(
453+
Builder sample,
454+
String rootPath,
455+
Set<String> includes,
456+
Set<String> excludes,
457+
boolean matchFieldNamesWithDots
458+
) throws IOException {
425459
try (XContentBuilder builtSample = sample.apply(createBuilder())) {
426460
BytesReference sampleBytes = BytesReference.bytes(builtSample);
427461
try (
428462
XContentParser parser = getXContentType().xContent()
429463
.createParser(
430-
XContentParserConfiguration.EMPTY.withFiltering(includes, excludes, matchFieldNamesWithDots),
464+
XContentParserConfiguration.EMPTY.withFiltering(rootPath, includes, excludes, matchFieldNamesWithDots),
431465
sampleBytes.streamInput()
432466
)
433467
) {

server/src/main/java/org/elasticsearch/action/update/UpdateHelper.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.elasticsearch.script.UpdateScript;
3737
import org.elasticsearch.script.UpsertCtxMap;
3838
import org.elasticsearch.search.lookup.Source;
39+
import org.elasticsearch.search.lookup.SourceFilter;
3940
import org.elasticsearch.xcontent.XContentType;
4041

4142
import java.io.IOException;
@@ -344,8 +345,9 @@ public static GetResult extractGetResult(
344345
return null;
345346
}
346347
BytesReference sourceFilteredAsBytes = sourceAsBytes;
347-
if (request.fetchSource().hasFilter()) {
348-
sourceFilteredAsBytes = Source.fromMap(source, sourceContentType).filter(request.fetchSource().filter()).internalSourceRef();
348+
SourceFilter sourceFilter = request.fetchSource().filter();
349+
if (sourceFilter != null) {
350+
sourceFilteredAsBytes = Source.fromMap(source, sourceContentType).filter(sourceFilter).internalSourceRef();
349351
}
350352

351353
// TODO when using delete/none, we can still return the source as bytes by generating it (using the sourceContentType)

server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,7 @@ public DataStream getParentDataStream() {
13611361
}
13621362

13631363
public static final XContentParserConfiguration TS_EXTRACT_CONFIG = XContentParserConfiguration.EMPTY.withFiltering(
1364+
null,
13641365
Set.of(TIMESTAMP_FIELD_NAME),
13651366
null,
13661367
false

server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public static class ExtractFromSource extends IndexRouting {
273273
trackTimeSeriesRoutingHash = metadata.getCreationVersion().onOrAfter(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID);
274274
List<String> routingPaths = metadata.getRoutingPaths();
275275
isRoutingPath = Regex.simpleMatcher(routingPaths.toArray(String[]::new));
276-
this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(Set.copyOf(routingPaths), null, true);
276+
this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, Set.copyOf(routingPaths), null, true);
277277
}
278278

279279
public boolean matchesField(String fieldName) {

server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public static Tuple<XContentType, Map<String, Object>> convertToMap(
191191
) throws ElasticsearchParseException {
192192
XContentParserConfiguration config = XContentParserConfiguration.EMPTY;
193193
if (include != null || exclude != null) {
194-
config = config.withFiltering(include, exclude, false);
194+
config = config.withFiltering(null, include, exclude, false);
195195
}
196196
return parseToType(ordered ? XContentParser::mapOrdered : XContentParser::map, bytes, xContentType, config);
197197
}
@@ -266,7 +266,10 @@ public static Map<String, Object> convertToMap(
266266
@Nullable Set<String> exclude
267267
) throws ElasticsearchParseException {
268268
try (
269-
XContentParser parser = xContent.createParser(XContentParserConfiguration.EMPTY.withFiltering(include, exclude, false), input)
269+
XContentParser parser = xContent.createParser(
270+
XContentParserConfiguration.EMPTY.withFiltering(null, include, exclude, false),
271+
input
272+
)
270273
) {
271274
return ordered ? parser.mapOrdered() : parser.map();
272275
} catch (IOException e) {
@@ -301,7 +304,7 @@ public static Map<String, Object> convertToMap(
301304
) throws ElasticsearchParseException {
302305
try (
303306
XContentParser parser = xContent.createParser(
304-
XContentParserConfiguration.EMPTY.withFiltering(include, exclude, false),
307+
XContentParserConfiguration.EMPTY.withFiltering(null, include, exclude, false),
305308
bytes,
306309
offset,
307310
length

0 commit comments

Comments
 (0)