Skip to content

Commit 6448c1c

Browse files
authored
ESQL load all dimensions when doing a Time Series query (#132687)
Fetch all dimensions when running a time series query Note on CCS - this is adding a filter to field caps, which will not be available on older versions. This will cause the CCS field caps request to fail for those clusters. However, since the TS command this supports will not be released until after this is merged, that will only happen on clusters that weren't going to be able to run the query anyway. This seems acceptable to me, but worth mentioning.
1 parent f846275 commit 6448c1c

File tree

7 files changed

+95
-14
lines changed

7 files changed

+95
-14
lines changed

server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ static Map<String, IndexFieldCapabilities> retrieveFieldCaps(
161161
boolean includeEmptyFields
162162
) {
163163
boolean includeParentObjects = checkIncludeParents(filters);
164+
boolean includeDimensions = checkIncludeDimensions(filters);
164165

165166
Predicate<MappedFieldType> filter = buildFilter(filters, types, context);
166167
boolean isTimeSeriesIndex = context.getIndexSettings().getTimestampBounds() != null;
@@ -169,10 +170,10 @@ static Map<String, IndexFieldCapabilities> retrieveFieldCaps(
169170
Map<String, IndexFieldCapabilities> responseMap = new HashMap<>();
170171
for (Map.Entry<String, MappedFieldType> entry : context.getAllFields()) {
171172
final String field = entry.getKey();
172-
if (fieldNameFilter.test(field) == false) {
173+
MappedFieldType ft = entry.getValue();
174+
if (fieldNameFilter.test(field) == false && ((includeDimensions && ft.isDimension()) == false)) {
173175
continue;
174176
}
175-
MappedFieldType ft = entry.getValue();
176177
if ((includeEmptyFields || ft.fieldHasValue(fieldInfos))
177178
&& (fieldPredicate.test(ft.name()) || context.isMetadataField(ft.name()))
178179
&& (filter == null || filter.test(ft))) {
@@ -234,6 +235,15 @@ private static boolean checkIncludeParents(String[] filters) {
234235
return true;
235236
}
236237

238+
private static boolean checkIncludeDimensions(String[] filters) {
239+
for (String filter : filters) {
240+
if ("+dimension".equals(filter)) {
241+
return true;
242+
}
243+
}
244+
return false;
245+
}
246+
237247
private static boolean canMatchShard(
238248
ShardId shardId,
239249
QueryBuilder indexFilter,
@@ -267,7 +277,8 @@ private static Predicate<MappedFieldType> buildFilter(String[] filters, String[]
267277
}
268278

269279
for (String filter : filters) {
270-
if ("parent".equals(filter) || "-parent".equals(filter)) {
280+
// These "filters" are handled differently, in that they are not ANDed with the field name pattern
281+
if ("parent".equals(filter) || "-parent".equals(filter) || "+dimension".equals(filter)) {
271282
continue;
272283
}
273284
Predicate<MappedFieldType> next = switch (filter) {

server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFilterTests.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import org.apache.lucene.index.FieldInfos;
1313
import org.elasticsearch.common.Strings;
14+
import org.elasticsearch.common.settings.Settings;
1415
import org.elasticsearch.index.mapper.MapperService;
1516
import org.elasticsearch.index.mapper.MapperServiceTestCase;
1617
import org.elasticsearch.index.query.SearchExecutionContext;
@@ -97,6 +98,54 @@ public void testMetadataFilters() throws IOException {
9798
}
9899
}
99100

101+
public void testDimensionFilters() throws IOException {
102+
MapperService mapperService = createMapperService(
103+
Settings.builder().put("index.mode", "time_series").put("index.routing_path", "dim.*").build(),
104+
"""
105+
{ "_doc" : {
106+
"properties" : {
107+
"metric" : { "type" : "long" },
108+
"dimension_1" : { "type" : "keyword", "time_series_dimension" : "true" },
109+
"dimension_2" : { "type" : "long", "time_series_dimension" : "true" }
110+
}
111+
} }
112+
"""
113+
);
114+
SearchExecutionContext sec = createSearchExecutionContext(mapperService);
115+
116+
{
117+
// First, test without the filter
118+
Map<String, IndexFieldCapabilities> response = FieldCapabilitiesFetcher.retrieveFieldCaps(
119+
sec,
120+
s -> s.equals("metric"),
121+
Strings.EMPTY_ARRAY,
122+
Strings.EMPTY_ARRAY,
123+
FieldPredicate.ACCEPT_ALL,
124+
getMockIndexShard(),
125+
true
126+
);
127+
assertNotNull(response.get("metric"));
128+
assertNull(response.get("dimension_1"));
129+
assertNull(response.get("dimension_2"));
130+
}
131+
132+
{
133+
// then, test with the filter
134+
Map<String, IndexFieldCapabilities> response = FieldCapabilitiesFetcher.retrieveFieldCaps(
135+
sec,
136+
s -> s.equals("metric"),
137+
new String[] { "+dimension" },
138+
Strings.EMPTY_ARRAY,
139+
FieldPredicate.ACCEPT_ALL,
140+
getMockIndexShard(),
141+
true
142+
);
143+
assertNotNull(response.get("dimension_1"));
144+
assertNotNull(response.get("dimension_2"));
145+
assertNotNull(response.get("metric"));
146+
}
147+
}
148+
100149
public void testExcludeMultifields() throws IOException {
101150
MapperService mapperService = createMapperService("""
102151
{ "_doc" : {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/EnrichPolicyResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ public void messageReceived(LookupRequest request, TransportChannel channel, Tas
449449
} else {
450450
failures.put(policyName, indexResult.toString());
451451
}
452-
}));
452+
}), false);
453453
}
454454
}
455455
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/UnresolvedRelation.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,12 @@ public List<Object> nodeProperties() {
166166
public String toString() {
167167
return UNRESOLVED_PREFIX + indexPattern.indexPattern();
168168
}
169+
170+
/**
171+
* @return true if and only if this relation is being loaded in "time series mode",
172+
* which changes a number of behaviors in the planner.
173+
*/
174+
public boolean isTimeSeriesMode() {
175+
return indexMode == IndexMode.TIME_SERIES;
176+
}
169177
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,8 @@ private void preAnalyzeLookupIndex(
424424
patternWithRemotes,
425425
fieldNames,
426426
null,
427-
listener.map(indexResolution -> receiveLookupIndexResolution(result, localPattern, executionInfo, indexResolution))
427+
listener.map(indexResolution -> receiveLookupIndexResolution(result, localPattern, executionInfo, indexResolution)),
428+
false
428429
);
429430
}
430431

@@ -638,8 +639,10 @@ private void preAnalyzeMainIndices(
638639
result.withIndexResolution(IndexResolution.valid(new EsIndex(table.indexPattern(), Map.of(), Map.of())))
639640
);
640641
} else {
642+
boolean includeAllDimensions = false;
641643
// call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types
642644
if (preAnalysis.indexMode == IndexMode.TIME_SERIES) {
645+
includeAllDimensions = true;
643646
// TODO: Maybe if no indices are returned, retry without index mode and provide a clearer error message.
644647
var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName());
645648
if (requestFilter != null) {
@@ -654,7 +657,8 @@ private void preAnalyzeMainIndices(
654657
requestFilter,
655658
listener.delegateFailure((l, indexResolution) -> {
656659
l.onResponse(result.withIndexResolution(indexResolution));
657-
})
660+
}),
661+
includeAllDimensions
658662
);
659663
}
660664
} else {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/FieldNameUtils.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package org.elasticsearch.xpack.esql.session;
99

1010
import org.elasticsearch.common.regex.Regex;
11-
import org.elasticsearch.index.IndexMode;
1211
import org.elasticsearch.xpack.esql.analysis.EnrichResolution;
1312
import org.elasticsearch.xpack.esql.core.expression.Alias;
1413
import org.elasticsearch.xpack.esql.core.expression.Attribute;
@@ -64,15 +63,15 @@ public class FieldNameUtils {
6463
public static PreAnalysisResult resolveFieldNames(LogicalPlan parsed, EnrichResolution enrichResolution) {
6564

6665
// we need the match_fields names from enrich policies and THEN, with an updated list of fields, we call field_caps API
67-
var enrichPolicyMatchFields = enrichResolution.resolvedEnrichPolicies()
66+
Set<String> enrichPolicyMatchFields = enrichResolution.resolvedEnrichPolicies()
6867
.stream()
6968
.map(ResolvedEnrichPolicy::matchField)
7069
.collect(Collectors.toSet());
7170

7271
// get the field names from the parsed plan combined with the ENRICH match fields from the ENRICH policy
7372
List<LogicalPlan> inlinestats = parsed.collect(InlineStats.class::isInstance);
7473
Set<Aggregate> inlinestatsAggs = new HashSet<>();
75-
for (var i : inlinestats) {
74+
for (LogicalPlan i : inlinestats) {
7675
inlinestatsAggs.add(((InlineStats) i).aggregate());
7776
}
7877

@@ -167,7 +166,7 @@ public static PreAnalysisResult resolveFieldNames(LogicalPlan parsed, EnrichReso
167166
}
168167
} else {
169168
referencesBuilder.get().addAll(p.references());
170-
if (p instanceof UnresolvedRelation ur && ur.indexMode() == IndexMode.TIME_SERIES) {
169+
if (p instanceof UnresolvedRelation ur && ur.isTimeSeriesMode()) {
171170
// METRICS aggs generally rely on @timestamp without the user having to mention it.
172171
referencesBuilder.get().add(new UnresolvedAttribute(ur.source(), MetadataAttribute.TIMESTAMP_FIELD));
173172
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,12 @@ public void resolveAsMergedMapping(
8181
String indexWildcard,
8282
Set<String> fieldNames,
8383
QueryBuilder requestFilter,
84-
ActionListener<IndexResolution> listener
84+
ActionListener<IndexResolution> listener,
85+
boolean includeAllDimensions
8586
) {
8687
client.execute(
8788
EsqlResolveFieldsAction.TYPE,
88-
createFieldCapsRequest(indexWildcard, fieldNames, requestFilter),
89+
createFieldCapsRequest(indexWildcard, fieldNames, requestFilter, includeAllDimensions),
8990
listener.delegateFailureAndWrap((l, response) -> l.onResponse(mergedMappings(indexWildcard, response)))
9091
);
9192
}
@@ -273,7 +274,12 @@ private static EsField conflictingMetricTypes(String name, String fullName, Fiel
273274
return new InvalidMappedField(name, "mapped as different metric types in indices: " + indices);
274275
}
275276

276-
private static FieldCapabilitiesRequest createFieldCapsRequest(String index, Set<String> fieldNames, QueryBuilder requestFilter) {
277+
private static FieldCapabilitiesRequest createFieldCapsRequest(
278+
String index,
279+
Set<String> fieldNames,
280+
QueryBuilder requestFilter,
281+
boolean includeAllDimensions
282+
) {
277283
FieldCapabilitiesRequest req = new FieldCapabilitiesRequest().indices(Strings.commaDelimitedListToStringArray(index));
278284
req.fields(fieldNames.toArray(String[]::new));
279285
req.includeUnmapped(true);
@@ -282,7 +288,11 @@ private static FieldCapabilitiesRequest createFieldCapsRequest(String index, Set
282288
// also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable
283289
req.indicesOptions(FIELD_CAPS_INDICES_OPTIONS);
284290
// we ignore the nested data type fields starting with https://github.com/elastic/elasticsearch/pull/111495
285-
req.filters("-nested");
291+
if (includeAllDimensions) {
292+
req.filters("-nested", "+dimension");
293+
} else {
294+
req.filters("-nested");
295+
}
286296
req.setMergeResults(false);
287297
return req;
288298
}

0 commit comments

Comments
 (0)