-
Notifications
You must be signed in to change notification settings - Fork 25.2k
ESQL: Add a pre-mapping logical plan processing step #120368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b09553a
cbddbc6
baec4ee
b17f6e6
8b5d4a8
8fbd0fc
62a5862
6040f3a
865efe5
aebbccf
364d7af
da358b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pr: 120368 | ||
summary: Add a pre-mapping logical plan processing step | ||
area: ES|QL | ||
type: enhancement | ||
issues: [] |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.esql.expression.function.fulltext; | ||
|
||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.action.ResolvedIndices; | ||
import org.elasticsearch.index.query.QueryBuilder; | ||
import org.elasticsearch.index.query.QueryRewriteContext; | ||
import org.elasticsearch.index.query.Rewriteable; | ||
import org.elasticsearch.xpack.esql.core.util.Holder; | ||
import org.elasticsearch.xpack.esql.plan.logical.EsRelation; | ||
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; | ||
import org.elasticsearch.xpack.esql.planner.TranslatorHandler; | ||
import org.elasticsearch.xpack.esql.planner.mapper.preprocessor.MappingPreProcessor; | ||
import org.elasticsearch.xpack.esql.plugin.TransportActionServices; | ||
import org.elasticsearch.xpack.esql.session.IndexResolver; | ||
|
||
import java.io.IOException; | ||
import java.util.HashSet; | ||
import java.util.Set; | ||
|
||
/** | ||
* Some {@link FullTextFunction} implementations such as {@link org.elasticsearch.xpack.esql.expression.function.fulltext.Match} | ||
* will be translated to a {@link QueryBuilder} that require a rewrite phase on the coordinator. | ||
* {@link FullTextFunctionMappingPreprocessor#preprocess(LogicalPlan, TransportActionServices, ActionListener)} will rewrite the plan by | ||
* replacing {@link FullTextFunction} expression with new ones that hold rewritten {@link QueryBuilder}s. | ||
*/ | ||
public final class FullTextFunctionMappingPreprocessor implements MappingPreProcessor { | ||
|
||
public static final FullTextFunctionMappingPreprocessor INSTANCE = new FullTextFunctionMappingPreprocessor(); | ||
|
||
private FullTextFunctionMappingPreprocessor() {} | ||
|
||
@Override | ||
public void preprocess(LogicalPlan plan, TransportActionServices services, ActionListener<LogicalPlan> listener) { | ||
Rewriteable.rewriteAndFetch( | ||
new FullTextFunctionsRewritable(plan), | ||
queryRewriteContext(services, indexNames(plan)), | ||
listener.delegateFailureAndWrap((l, r) -> l.onResponse(r.plan)) | ||
); | ||
} | ||
|
||
private static QueryRewriteContext queryRewriteContext(TransportActionServices services, Set<String> indexNames) { | ||
ResolvedIndices resolvedIndices = ResolvedIndices.resolveWithIndexNamesAndOptions( | ||
indexNames.toArray(String[]::new), | ||
IndexResolver.FIELD_CAPS_INDICES_OPTIONS, | ||
services.clusterService().state(), | ||
services.indexNameExpressionResolver(), | ||
services.transportService().getRemoteClusterService(), | ||
System.currentTimeMillis() | ||
); | ||
|
||
return services.searchService().getRewriteContext(System::currentTimeMillis, resolvedIndices, null); | ||
} | ||
|
||
public Set<String> indexNames(LogicalPlan plan) { | ||
Set<String> indexNames = new HashSet<>(); | ||
plan.forEachDown(EsRelation.class, esRelation -> indexNames.addAll(esRelation.concreteIndices())); | ||
return indexNames; | ||
} | ||
|
||
private record FullTextFunctionsRewritable(LogicalPlan plan) | ||
implements | ||
Rewriteable<FullTextFunctionMappingPreprocessor.FullTextFunctionsRewritable> { | ||
@Override | ||
public FullTextFunctionsRewritable rewrite(QueryRewriteContext ctx) throws IOException { | ||
Holder<IOException> exceptionHolder = new Holder<>(); | ||
Holder<Boolean> updated = new Holder<>(false); | ||
LogicalPlan newPlan = plan.transformExpressionsDown(FullTextFunction.class, f -> { | ||
QueryBuilder builder = f.queryBuilder(), initial = builder; | ||
builder = builder == null ? f.asQuery(TranslatorHandler.TRANSLATOR_HANDLER).asBuilder() : builder; | ||
try { | ||
builder = builder.rewrite(ctx); | ||
} catch (IOException e) { | ||
exceptionHolder.trySet(e); | ||
} | ||
var rewritten = builder != initial; | ||
updated.set(updated.get() || rewritten); | ||
return rewritten ? f.replaceQueryBuilder(builder) : f; | ||
}); | ||
if (exceptionHolder.get() != null) { | ||
throw exceptionHolder.get(); | ||
} | ||
return updated.get() ? new FullTextFunctionsRewritable(newPlan) : this; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.esql.planner.mapper.preprocessor; | ||
|
||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; | ||
import org.elasticsearch.xpack.esql.plugin.TransportActionServices; | ||
|
||
/** | ||
* Interface for a LogicalPlan processing rule occurring after the optimization, but before mapping to a physical plan. | ||
* This step occurs on the coordinator. The rule may use services provided to the transport action and thus can resolve indices, rewrite | ||
* queries, perform substitutions, etc. | ||
* Note that the LogicalPlan following the rules' changes will not undergo another logical optimization round. The changes these rules | ||
* should apply are only those that require access to services that need to be performed asynchronously. | ||
*/ | ||
public interface MappingPreProcessor { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will there be multiple preprocessors? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That was my assumption, yes.
There are some particularities to how queries are repeatedly rewritten (and stopping that is done by identity checking of a return object) -- these should remain particular to But the execution of the preprocessor is done at a dedicated stage. Not sure if the current format meets your expectation, but happy to shape it further if it can be done better. |
||
|
||
/** | ||
* Process a logical plan making use of the available services and provide the updated plan to the provided listener. | ||
* @param plan the logical plan to process | ||
* @param services the services available from the transport action | ||
* @param listener the listener to notify when processing is complete | ||
*/ | ||
void preprocess(LogicalPlan plan, TransportActionServices services, ActionListener<LogicalPlan> listener); | ||
|
||
interface MappingPreProcessorSupplier { | ||
MappingPreProcessor mappingPreProcessor(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.esql.planner.mapper.preprocessor; | ||
|
||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; | ||
import org.elasticsearch.xpack.esql.plugin.TransportActionServices; | ||
|
||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Set; | ||
|
||
public class MappingPreprocessorExecutor { | ||
|
||
private final TransportActionServices services; | ||
|
||
public MappingPreprocessorExecutor(TransportActionServices services) { | ||
this.services = services; | ||
} | ||
|
||
public void execute(LogicalPlan plan, ActionListener<LogicalPlan> listener) { | ||
execute(plan, preprocessors(plan), 0, listener); | ||
} | ||
|
||
private static List<MappingPreProcessor> preprocessors(LogicalPlan plan) { | ||
Set<MappingPreProcessor> preprocessors = new HashSet<>(); | ||
plan.forEachExpressionDown(e -> { | ||
if (e instanceof MappingPreProcessor.MappingPreProcessorSupplier supplier) { | ||
preprocessors.add(supplier.mappingPreProcessor()); | ||
} | ||
}); | ||
return List.copyOf(preprocessors); | ||
} | ||
|
||
private void execute(LogicalPlan plan, List<MappingPreProcessor> preprocessors, int index, ActionListener<LogicalPlan> listener) { | ||
if (index == preprocessors.size()) { | ||
listener.onResponse(plan); | ||
} else { | ||
preprocessors.get(index) | ||
.preprocess(plan, services, listener.delegateFailureAndWrap((l, p) -> execute(p, preprocessors, index + 1, l))); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.esql.plugin; | ||
|
||
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; | ||
import org.elasticsearch.cluster.service.ClusterService; | ||
import org.elasticsearch.compute.operator.exchange.ExchangeService; | ||
import org.elasticsearch.search.SearchService; | ||
import org.elasticsearch.transport.TransportService; | ||
import org.elasticsearch.usage.UsageService; | ||
|
||
public record TransportActionServices( | ||
TransportService transportService, | ||
SearchService searchService, | ||
ExchangeService exchangeService, | ||
ClusterService clusterService, | ||
IndexNameExpressionResolver indexNameExpressionResolver, | ||
UsageService usageService | ||
) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch