Skip to content
5 changes: 5 additions & 0 deletions docs/changelog/117246.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 117246
summary: LOOKUP JOIN using field-caps for field mapping
area: ES|QL
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class CsvTestsDataLoader {
private static final TestsDataset APPS = new TestsDataset("apps");
private static final TestsDataset APPS_SHORT = APPS.withIndex("apps_short").withTypeMapping(Map.of("id", "short"));
private static final TestsDataset LANGUAGES = new TestsDataset("languages");
private static final TestsDataset LANGUAGES_LOOKUP = LANGUAGES.withIndex("languages_lookup")
.withSetting("languages_lookup-settings.json");
private static final TestsDataset ALERTS = new TestsDataset("alerts");
private static final TestsDataset UL_LOGS = new TestsDataset("ul_logs");
private static final TestsDataset SAMPLE_DATA = new TestsDataset("sample_data");
Expand Down Expand Up @@ -93,14 +95,13 @@ public class CsvTestsDataLoader {
private static final TestsDataset BOOKS = new TestsDataset("books");
private static final TestsDataset SEMANTIC_TEXT = new TestsDataset("semantic_text").withInferenceEndpoint(true);

private static final String LOOKUP_INDEX_SUFFIX = "_lookup";

public static final Map<String, TestsDataset> CSV_DATASET_MAP = Map.ofEntries(
Map.entry(EMPLOYEES.indexName, EMPLOYEES),
Map.entry(HOSTS.indexName, HOSTS),
Map.entry(APPS.indexName, APPS),
Map.entry(APPS_SHORT.indexName, APPS_SHORT),
Map.entry(LANGUAGES.indexName, LANGUAGES),
Map.entry(LANGUAGES_LOOKUP.indexName, LANGUAGES_LOOKUP),
Map.entry(UL_LOGS.indexName, UL_LOGS),
Map.entry(SAMPLE_DATA.indexName, SAMPLE_DATA),
Map.entry(ALERTS.indexName, ALERTS),
Expand Down Expand Up @@ -130,9 +131,7 @@ public class CsvTestsDataLoader {
Map.entry(DISTANCES.indexName, DISTANCES),
Map.entry(ADDRESSES.indexName, ADDRESSES),
Map.entry(BOOKS.indexName, BOOKS),
Map.entry(SEMANTIC_TEXT.indexName, SEMANTIC_TEXT),
// JOIN LOOKUP alias
Map.entry(LANGUAGES.indexName + LOOKUP_INDEX_SUFFIX, LANGUAGES.withIndex(LANGUAGES.indexName + LOOKUP_INDEX_SUFFIX))
Map.entry(SEMANTIC_TEXT.indexName, SEMANTIC_TEXT)
);

private static final EnrichConfig LANGUAGES_ENRICH = new EnrichConfig("languages_policy", "enrich-policy-languages.json");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"index": {
"mode": "lookup"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.EsqlArithmeticOperation;
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
import org.elasticsearch.xpack.esql.index.EsIndex;
import org.elasticsearch.xpack.esql.index.IndexResolution;
import org.elasticsearch.xpack.esql.parser.ParsingException;
import org.elasticsearch.xpack.esql.plan.TableIdentifier;
import org.elasticsearch.xpack.esql.plan.logical.Aggregate;
Expand Down Expand Up @@ -106,7 +107,6 @@
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
Expand Down Expand Up @@ -199,57 +199,42 @@ private static class ResolveTable extends ParameterizedAnalyzerRule<UnresolvedRe

@Override
protected LogicalPlan rule(UnresolvedRelation plan, AnalyzerContext context) {
if (plan.indexMode().equals(IndexMode.LOOKUP)) {
return hackLookupMapping(plan);
}
if (context.indexResolution().isValid() == false) {
return plan.unresolvedMessage().equals(context.indexResolution().toString())
return resolveIndex(plan, plan.indexMode().equals(IndexMode.LOOKUP) ? context.lookupResolution() : context.indexResolution());
}

private LogicalPlan resolveIndex(UnresolvedRelation plan, IndexResolution indexResolution) {
if (indexResolution.isValid() == false) {
return plan.unresolvedMessage().equals(indexResolution.toString())
? plan
: new UnresolvedRelation(
plan.source(),
plan.table(),
plan.frozen(),
plan.metadataFields(),
plan.indexMode(),
context.indexResolution().toString(),
indexResolution.toString(),
plan.commandName()
);
}
TableIdentifier table = plan.table();
if (context.indexResolution().matches(table.index()) == false) {
if (indexResolution.matches(table.index()) == false) {
// TODO: fix this (and tests), or drop check (seems SQL-inherited, where's also defective)
new UnresolvedRelation(
plan.source(),
plan.table(),
plan.frozen(),
plan.metadataFields(),
plan.indexMode(),
"invalid [" + table + "] resolution to [" + context.indexResolution() + "]",
"invalid [" + table + "] resolution to [" + indexResolution + "]",
plan.commandName()
);
}

EsIndex esIndex = context.indexResolution().get();
EsIndex esIndex = indexResolution.get();
var attributes = mappingAsAttributes(plan.source(), esIndex.mapping());
attributes.addAll(plan.metadataFields());
return new EsRelation(plan.source(), esIndex, attributes.isEmpty() ? NO_FIELDS : attributes, plan.indexMode());
}

private LogicalPlan hackLookupMapping(UnresolvedRelation plan) {
if (plan.table().index().toLowerCase(Locale.ROOT).equals("languages_lookup")) {
EsIndex esIndex = new EsIndex(
"languages_lookup",
Map.ofEntries(
Map.entry("language_code", new EsField("language_code", DataType.LONG, Map.of(), true)),
Map.entry("language_name", new EsField("language", DataType.KEYWORD, Map.of(), true))
),
Map.of("languages_lookup", IndexMode.LOOKUP)
);
var attributes = mappingAsAttributes(plan.source(), esIndex.mapping());
return new EsRelation(plan.source(), esIndex, attributes.isEmpty() ? NO_FIELDS : attributes, plan.indexMode());
}
return plan;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,17 @@ public record AnalyzerContext(
Configuration configuration,
EsqlFunctionRegistry functionRegistry,
IndexResolution indexResolution,
IndexResolution lookupResolution,
EnrichResolution enrichResolution
) {}
) {
// Currently for tests only, since most do not test lookups
// TODO: make this even simpler, remove the enrichResolution for tests that do not require it (most tests)
public AnalyzerContext(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for testing-only use? Would it be worth creating an instance for those purposes? Or a method as a testing util creating it? Or maybe leaving a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for tests, but I did not want to follow the test approach to enrichResolution, which has three different ways of passing in an empty resolution, cluttering the tests. So this is an intermediate solution, which minimizes code changes (does not change test code), while getting the goal of the PR achieved. Then I would like a second PR that simplifies both this and the enrich resolution for tests (and cleans up test code all over the place).

Configuration configuration,
EsqlFunctionRegistry functionRegistry,
IndexResolution indexResolution,
EnrichResolution enrichResolution
) {
this(configuration, functionRegistry, indexResolution, IndexResolution.invalid("<none>"), enrichResolution);
}
}
Loading