Skip to content

Commit 2197641

Browse files
craigtavernerleemthompoalex-spies
authored andcommitted
Support types table in lookup join docs (#130410)
* Support types table in lookup join docs * Don't show a results column in the join types * Make LOOKUP JOIN types table more compact * Update docs/reference/query-languages/esql/esql-lookup-join.md Co-authored-by: Liam Thompson <[email protected]> Co-authored-by: Alexander Spies <[email protected]>
1 parent a67c013 commit 2197641

File tree

6 files changed

+207
-18
lines changed

6 files changed

+207
-18
lines changed

docs/reference/query-languages/esql/_snippets/commands/layout/lookup-join.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ results, the output will contain one row for each matching combination.
4242
For important information about using `LOOKUP JOIN`, refer to [Usage notes](../../../../esql/esql-lookup-join.md#usage-notes).
4343
::::
4444

45+
:::{include} ../types/lookup-join.md
46+
:::
47+
4548
**Examples**
4649

4750
**IP Threat correlation**: This query would allow you to see if any source
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
2+
3+
**Supported types**
4+
5+
| field from the left index | field from the lookup index |
6+
| --- | --- |
7+
| boolean | boolean |
8+
| byte | half_float, float, double, scaled_float, byte, short, integer, long |
9+
| date | date |
10+
| date_nanos | date_nanos |
11+
| double | half_float, float, double, scaled_float, byte, short, integer, long |
12+
| float | half_float, float, double, scaled_float, byte, short, integer, long |
13+
| half_float | half_float, float, double, scaled_float, byte, short, integer, long |
14+
| integer | half_float, float, double, scaled_float, byte, short, integer, long |
15+
| ip | ip |
16+
| keyword | keyword |
17+
| long | half_float, float, double, scaled_float, byte, short, integer, long |
18+
| scaled_float | half_float, float, double, scaled_float, byte, short, integer, long |
19+
| short | half_float, float, double, scaled_float, byte, short, integer, long |
20+
| text | keyword |
21+

docs/reference/query-languages/esql/esql-lookup-join.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,38 @@ Refer to the examples section of the [`LOOKUP JOIN`](/reference/query-languages/
142142

143143
## Prerequisites [esql-lookup-join-prereqs]
144144

145-
To use `LOOKUP JOIN`, the following requirements must be met:
145+
### Index configuration
146146

147-
* Indices used for lookups must be configured with the [`lookup` index mode](/reference/elasticsearch/index-settings/index-modules.md#index-mode-setting)
148-
* **Compatible data types**: The join key and join field in the lookup index must have compatible data types. This means:
149-
* The data types must either be identical or be internally represented as the same type in {{esql}}
150-
* Numeric types follow these compatibility rules:
151-
* `short` and `byte` are compatible with `integer` (all represented as `int`)
152-
* `float`, `half_float`, and `scaled_float` are compatible with `double` (all represented as `double`)
153-
* For text fields: You can only use text fields as the join key on the left-hand side of the join and only if they have a `.keyword` subfield
147+
Indices used for lookups must be configured with the [`lookup` index mode](/reference/elasticsearch/index-settings/index-modules.md#index-mode-setting).
154148

149+
### Data type compatibility
150+
151+
Join keys must have compatible data types between the source and lookup indices. Types within the same compatibility group can be joined together:
152+
153+
| Compatibility group | Types | Notes |
154+
|------------------------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
155+
| **Numeric family** | `byte`, `short`, `integer`, `long`, `half_float`, `float`, `scaled_float`, `double` | All compatible |
156+
| **Keyword family** | `keyword`, `text.keyword` | Text fields only as join key on left-hand side and must have `.keyword` subfield |
157+
| **Date (Exact)** | `date` | Must match exactly |
158+
| **Date Nanos (Exact)** | `date_nanos` | Must match exactly |
159+
| **Boolean** | `boolean` | Must match exactly |
160+
161+
```{tip}
155162
To obtain a join key with a compatible type, use a [conversion function](/reference/query-languages/esql/functions-operators/type-conversion-functions.md) if needed.
163+
```
156164

157-
For a complete list of supported data types and their internal representations, see the [Supported Field Types documentation](/reference/query-languages/esql/limitations.md#_supported_types).
165+
### Unsupported Types
166+
167+
In addition to the [{{esql}} unsupported field types](/reference/query-languages/esql/limitations.md#_unsupported_types), `LOOKUP JOIN` does not support:
168+
169+
* `VERSION`
170+
* `UNSIGNED_LONG`
171+
* Spatial types like `GEO_POINT`, `GEO_SHAPE`
172+
* Temporal intervals like `DURATION`, `PERIOD`
173+
174+
```{note}
175+
For a complete list of all types supported in `LOOKUP JOIN`, refer to the [`LOOKUP JOIN` supported types table](/reference/query-languages/esql/commands/processing-commands.md#esql-lookup-join).
176+
```
158177

159178
## Usage notes
160179

x-pack/plugin/esql/build.gradle

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,47 @@ tasks.named("test").configure {
226226
}
227227
}
228228

229+
// This is similar to the test task above, but needed for the LookupJoinTypesIT which runs in the internalClusterTest task
230+
// and generates a types table for the LOOKUP JOIN command. It is possible in future we might have move tests that do this.
231+
tasks.named("internalClusterTest").configure {
232+
if (buildParams.ci == false) {
233+
systemProperty 'generateDocs', true
234+
def injected = project.objects.newInstance(Injected)
235+
// Define the folder to delete and recreate
236+
def tempDir = file("build/testrun/internalClusterTest/temp/esql")
237+
doFirst {
238+
injected.fs.delete {
239+
it.delete(tempDir)
240+
}
241+
// Re-create this folder so we can save a table of generated examples to extract from csv-spec tests
242+
tempDir.mkdirs() // Recreate the folder
243+
}
244+
File snippetsFolder = file("build/testrun/internalClusterTest/temp/esql/_snippets")
245+
def snippetsDocFolder = file("${rootDir}/docs/reference/query-languages/esql/_snippets")
246+
def snippetsTree = fileTree(snippetsFolder).matching {
247+
include "**/types/*.md" // Recursively include all types/*.md files (effectively counting functions and operators)
248+
}
249+
250+
doLast {
251+
def snippets = snippetsTree.files.collect { it.name }
252+
int countSnippets = snippets.size()
253+
if (countSnippets == 0) {
254+
logger.quiet("ESQL Docs: No function/operator snippets created. Skipping sync.")
255+
} else {
256+
logger.quiet("ESQL Docs: Found $countSnippets generated function/operator snippets to patch into docs")
257+
injected.fs.sync {
258+
from snippetsFolder
259+
into snippetsDocFolder
260+
include '**/*.md'
261+
preserve {
262+
include '**/*.md'
263+
}
264+
}
265+
}
266+
}
267+
}
268+
}
269+
229270
/****************************************************************
230271
* Enable QA/rest integration tests for snapshot builds only *
231272
* TODO: Enable for all builds upon this feature release *

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupJoinTypesIT.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.elasticsearch.xpack.core.esql.action.ColumnInfo;
2020
import org.elasticsearch.xpack.esql.VerificationException;
2121
import org.elasticsearch.xpack.esql.core.type.DataType;
22+
import org.elasticsearch.xpack.esql.expression.function.DocsV3Support;
23+
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
2224
import org.elasticsearch.xpack.esql.plan.logical.join.Join;
2325
import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
2426
import org.elasticsearch.xpack.spatial.SpatialPlugin;
@@ -36,6 +38,7 @@
3638
import java.util.Map;
3739
import java.util.Set;
3840
import java.util.function.Consumer;
41+
import java.util.function.Supplier;
3942
import java.util.stream.Collectors;
4043

4144
import static org.elasticsearch.test.ESIntegTestCase.Scope.SUITE;
@@ -265,6 +268,22 @@ private static boolean existingIndex(Collection<TestConfigs> existing, DataType
265268
return existing.stream().anyMatch(c -> c.exists(indexName));
266269
}
267270

271+
/** This test generates documentation for the supported output types of the lookup join. */
272+
public void testOutputSupportedTypes() throws Exception {
273+
Map<List<DataType>, DataType> signatures = new LinkedHashMap<>();
274+
for (TestConfigs configs : testConfigurations.values()) {
275+
if (configs.group.equals("unsupported") || configs.group.equals("union-types")) {
276+
continue;
277+
}
278+
for (TestConfig config : configs.configs.values()) {
279+
if (config instanceof TestConfigPasses) {
280+
signatures.put(List.of(config.mainType(), config.lookupType()), null);
281+
}
282+
}
283+
}
284+
saveJoinTypes(() -> signatures);
285+
}
286+
268287
public void testLookupJoinStrings() {
269288
testLookupJoinTypes("strings");
270289
}
@@ -747,4 +766,18 @@ public void doTest() {
747766
private boolean isValidDataType(DataType dataType) {
748767
return UNDER_CONSTRUCTION.get(dataType) == null || UNDER_CONSTRUCTION.get(dataType).isEnabled();
749768
}
769+
770+
private static void saveJoinTypes(Supplier<Map<List<DataType>, DataType>> signatures) throws Exception {
771+
ArrayList<EsqlFunctionRegistry.ArgSignature> args = new ArrayList<>();
772+
args.add(new EsqlFunctionRegistry.ArgSignature("field from the left index", null, null, false, false));
773+
args.add(new EsqlFunctionRegistry.ArgSignature("field from the lookup index", null, null, false, false));
774+
DocsV3Support.CommandsDocsSupport docs = new DocsV3Support.CommandsDocsSupport(
775+
"lookup-join",
776+
LookupJoinTypesIT.class,
777+
null,
778+
args,
779+
signatures
780+
);
781+
docs.renderDocs();
782+
}
750783
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@
6262
import java.util.List;
6363
import java.util.Locale;
6464
import java.util.Map;
65+
import java.util.Objects;
6566
import java.util.Optional;
67+
import java.util.TreeMap;
6668
import java.util.function.Function;
6769
import java.util.function.Supplier;
6870
import java.util.regex.Matcher;
@@ -310,7 +312,7 @@ public License.OperationMode invoke(List<DataType> fieldTypes) throws Exception
310312
protected final String name;
311313
protected final FunctionDefinition definition;
312314
protected final Logger logger;
313-
private final Supplier<Map<List<DataType>, DataType>> signatures;
315+
protected final Supplier<Map<List<DataType>, DataType>> signatures;
314316
private TempFileWriter tempFileWriter;
315317
private final LicenseRequirementChecker licenseChecker;
316318

@@ -859,9 +861,11 @@ private ObservabilityTier getObservabilityTier() {
859861
/** Command specific docs generating, currently very empty since we only render kibana definition files */
860862
public static class CommandsDocsSupport extends DocsV3Support {
861863
private final LogicalPlan command;
864+
private List<EsqlFunctionRegistry.ArgSignature> args;
862865
private final XPackLicenseState licenseState;
863866
private final ObservabilityTier observabilityTier;
864867

868+
/** Used in CommandLicenseTests to generate Kibana docs with licensing information for commands */
865869
public CommandsDocsSupport(
866870
String name,
867871
Class<?> testClass,
@@ -875,15 +879,36 @@ public CommandsDocsSupport(
875879
this.observabilityTier = observabilityTier;
876880
}
877881

882+
/** Used in LookupJoinTypesIT to generate table of supported types for join field */
883+
public CommandsDocsSupport(
884+
String name,
885+
Class<?> testClass,
886+
LogicalPlan command,
887+
List<EsqlFunctionRegistry.ArgSignature> args,
888+
Supplier<Map<List<DataType>, DataType>> signatures
889+
) {
890+
super("commands", name, testClass, signatures);
891+
this.command = command;
892+
this.args = args;
893+
this.licenseState = null;
894+
this.observabilityTier = null;
895+
}
896+
878897
@Override
879898
public void renderSignature() throws IOException {
880899
// Unimplemented until we make command docs dynamically generated
881900
}
882901

883902
@Override
884903
public void renderDocs() throws Exception {
885-
// Currently we only render kibana definition files, but we could expand to rendering much more if we decide to
886-
renderKibanaCommandDefinition();
904+
// Currently we only render either signatures or kibana definition files,
905+
// but we could expand to rendering much more if we decide to
906+
if (args != null) {
907+
renderTypes(name, args);
908+
}
909+
if (licenseState != null) {
910+
renderKibanaCommandDefinition();
911+
}
887912
}
888913

889914
void renderKibanaCommandDefinition() throws Exception {
@@ -906,6 +931,47 @@ void renderKibanaCommandDefinition() throws Exception {
906931
writeToTempKibanaDir("definition", "json", rendered);
907932
}
908933
}
934+
935+
@Override
936+
void renderTypes(String name, List<EsqlFunctionRegistry.ArgSignature> args) throws IOException {
937+
assert args.size() == 2;
938+
StringBuilder header = new StringBuilder("| ");
939+
StringBuilder separator = new StringBuilder("| ");
940+
List<String> argNames = args.stream().map(EsqlFunctionRegistry.ArgSignature::name).toList();
941+
for (String arg : argNames) {
942+
header.append(arg).append(" | ");
943+
separator.append("---").append(" | ");
944+
}
945+
946+
Map<String, List<String>> compactedTable = new TreeMap<>();
947+
for (Map.Entry<List<DataType>, DataType> sig : this.signatures.get().entrySet()) {
948+
if (shouldHideSignature(sig.getKey(), sig.getValue())) {
949+
continue;
950+
}
951+
String mainType = sig.getKey().getFirst().esNameIfPossible();
952+
String secondaryType = sig.getKey().get(1).esNameIfPossible();
953+
List<String> secondaryTypes = compactedTable.computeIfAbsent(mainType, (k) -> new ArrayList<>());
954+
secondaryTypes.add(secondaryType);
955+
}
956+
957+
List<String> table = new ArrayList<>();
958+
for (Map.Entry<String, List<String>> sig : compactedTable.entrySet()) {
959+
String row = "| " + sig.getKey() + " | " + String.join(", ", sig.getValue()) + " |";
960+
table.add(row);
961+
}
962+
Collections.sort(table);
963+
if (table.isEmpty()) {
964+
logger.info("Warning: No table of types generated for [{}]", name);
965+
return;
966+
}
967+
968+
String rendered = DOCS_WARNING + """
969+
**Supported types**
970+
971+
""" + header + "\n" + separator + "\n" + String.join("\n", table) + "\n\n";
972+
logger.info("Writing function types for [{}]:\n{}", name, rendered);
973+
writeToTempSnippetsDir("types", rendered);
974+
}
909975
}
910976

911977
protected String buildFunctionSignatureSvg() throws IOException {
@@ -927,15 +993,18 @@ void renderParametersList(List<String> argNames, List<String> argDescriptions) t
927993
}
928994

929995
void renderTypes(String name, List<EsqlFunctionRegistry.ArgSignature> args) throws IOException {
996+
boolean showResultColumn = signatures.get().values().stream().anyMatch(Objects::nonNull);
930997
StringBuilder header = new StringBuilder("| ");
931998
StringBuilder separator = new StringBuilder("| ");
932999
List<String> argNames = args.stream().map(EsqlFunctionRegistry.ArgSignature::name).toList();
9331000
for (String arg : argNames) {
9341001
header.append(arg).append(" | ");
9351002
separator.append("---").append(" | ");
9361003
}
937-
header.append("result |");
938-
separator.append("--- |");
1004+
if (showResultColumn) {
1005+
header.append("result |");
1006+
separator.append("--- |");
1007+
}
9391008

9401009
List<String> table = new ArrayList<>();
9411010
for (Map.Entry<List<DataType>, DataType> sig : this.signatures.get().entrySet()) { // TODO flip to using sortedSignatures
@@ -945,7 +1014,7 @@ void renderTypes(String name, List<EsqlFunctionRegistry.ArgSignature> args) thro
9451014
if (sig.getKey().size() > argNames.size()) { // skip variadic [test] cases (but not those with optional parameters)
9461015
continue;
9471016
}
948-
table.add(getTypeRow(args, sig, argNames));
1017+
table.add(getTypeRow(args, sig, argNames, showResultColumn));
9491018
}
9501019
Collections.sort(table);
9511020
if (table.isEmpty()) {
@@ -964,7 +1033,8 @@ void renderTypes(String name, List<EsqlFunctionRegistry.ArgSignature> args) thro
9641033
private static String getTypeRow(
9651034
List<EsqlFunctionRegistry.ArgSignature> args,
9661035
Map.Entry<List<DataType>, DataType> sig,
967-
List<String> argNames
1036+
List<String> argNames,
1037+
boolean showResultColumn
9681038
) {
9691039
StringBuilder b = new StringBuilder("| ");
9701040
for (int i = 0; i < sig.getKey().size(); i++) {
@@ -978,8 +1048,10 @@ private static String getTypeRow(
9781048
b.append(" | ");
9791049
}
9801050
b.append("| ".repeat(argNames.size() - sig.getKey().size()));
981-
b.append(sig.getValue().esNameIfPossible());
982-
b.append(" |");
1051+
if (showResultColumn) {
1052+
b.append(sig.getValue().esNameIfPossible());
1053+
b.append(" |");
1054+
}
9831055
return b.toString();
9841056
}
9851057

0 commit comments

Comments
 (0)