Skip to content

Commit b40a520

Browse files
authored
Add Optional Source Filtering to Source Loaders (#113827)
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 a054bbc commit b40a520

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
@@ -35,6 +35,7 @@
3535
import org.elasticsearch.script.UpdateScript;
3636
import org.elasticsearch.script.UpsertCtxMap;
3737
import org.elasticsearch.search.lookup.Source;
38+
import org.elasticsearch.search.lookup.SourceFilter;
3839
import org.elasticsearch.xcontent.XContentType;
3940

4041
import java.io.IOException;
@@ -340,8 +341,9 @@ public static GetResult extractGetResult(
340341
return null;
341342
}
342343
BytesReference sourceFilteredAsBytes = sourceAsBytes;
343-
if (request.fetchSource().hasFilter()) {
344-
sourceFilteredAsBytes = Source.fromMap(source, sourceContentType).filter(request.fetchSource().filter()).internalSourceRef();
344+
SourceFilter sourceFilter = request.fetchSource().filter();
345+
if (sourceFilter != null) {
346+
sourceFilteredAsBytes = Source.fromMap(source, sourceContentType).filter(sourceFilter).internalSourceRef();
345347
}
346348

347349
// 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
@@ -283,7 +283,7 @@ public static class ExtractFromSource extends IndexRouting {
283283
trackTimeSeriesRoutingHash = metadata.getCreationVersion().onOrAfter(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID);
284284
List<String> routingPaths = metadata.getRoutingPaths();
285285
isRoutingPath = Regex.simpleMatcher(routingPaths.toArray(String[]::new));
286-
this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(Set.copyOf(routingPaths), null, true);
286+
this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, Set.copyOf(routingPaths), null, true);
287287
}
288288

289289
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
@@ -192,7 +192,7 @@ public static Tuple<XContentType, Map<String, Object>> convertToMap(
192192
) throws ElasticsearchParseException {
193193
XContentParserConfiguration config = XContentParserConfiguration.EMPTY;
194194
if (include != null || exclude != null) {
195-
config = config.withFiltering(include, exclude, false);
195+
config = config.withFiltering(null, include, exclude, false);
196196
}
197197
return parseToType(ordered ? XContentParser::mapOrdered : XContentParser::map, bytes, xContentType, config);
198198
}
@@ -267,7 +267,10 @@ public static Map<String, Object> convertToMap(
267267
@Nullable Set<String> exclude
268268
) throws ElasticsearchParseException {
269269
try (
270-
XContentParser parser = xContent.createParser(XContentParserConfiguration.EMPTY.withFiltering(include, exclude, false), input)
270+
XContentParser parser = xContent.createParser(
271+
XContentParserConfiguration.EMPTY.withFiltering(null, include, exclude, false),
272+
input
273+
)
271274
) {
272275
return ordered ? parser.mapOrdered() : parser.map();
273276
} catch (IOException e) {
@@ -302,7 +305,7 @@ public static Map<String, Object> convertToMap(
302305
) throws ElasticsearchParseException {
303306
try (
304307
XContentParser parser = xContent.createParser(
305-
XContentParserConfiguration.EMPTY.withFiltering(include, exclude, false),
308+
XContentParserConfiguration.EMPTY.withFiltering(null, include, exclude, false),
306309
bytes,
307310
offset,
308311
length

0 commit comments

Comments
 (0)