Skip to content

Commit f6547d6

Browse files
authored
ES|QL - Allow multivalued query parameters (#134317)
1 parent ab59e5e commit f6547d6

File tree

10 files changed

+651
-102
lines changed

10 files changed

+651
-102
lines changed

docs/changelog/134317.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 134317
2+
summary: ES|QL - Allow multivalued query parameters
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collection;
2323
import java.util.Collections;
2424
import java.util.Comparator;
25+
import java.util.List;
2526
import java.util.Locale;
2627
import java.util.Map;
2728
import java.util.Optional;
@@ -456,41 +457,51 @@ public static DataType fromEs(String name) {
456457
}
457458

458459
public static DataType fromJava(Object value) {
459-
if (value == null) {
460-
return NULL;
461-
}
462-
if (value instanceof Integer) {
463-
return INTEGER;
464-
}
465-
if (value instanceof Long) {
466-
return LONG;
467-
}
468-
if (value instanceof BigInteger) {
469-
return UNSIGNED_LONG;
470-
}
471-
if (value instanceof Boolean) {
472-
return BOOLEAN;
473-
}
474-
if (value instanceof Double) {
475-
return DOUBLE;
476-
}
477-
if (value instanceof Float) {
478-
return FLOAT;
479-
}
480-
if (value instanceof Byte) {
481-
return BYTE;
482-
}
483-
if (value instanceof Short) {
484-
return SHORT;
485-
}
486-
if (value instanceof ZonedDateTime) {
487-
return DATETIME;
488-
}
489-
if (value instanceof String || value instanceof Character || value instanceof BytesRef) {
490-
return KEYWORD;
460+
switch (value) {
461+
case null -> {
462+
return NULL;
463+
}
464+
case Integer i -> {
465+
return INTEGER;
466+
}
467+
case Long l -> {
468+
return LONG;
469+
}
470+
case BigInteger bigInteger -> {
471+
return UNSIGNED_LONG;
472+
}
473+
case Boolean b -> {
474+
return BOOLEAN;
475+
}
476+
case Double v -> {
477+
return DOUBLE;
478+
}
479+
case Float v -> {
480+
return FLOAT;
481+
}
482+
case Byte b -> {
483+
return BYTE;
484+
}
485+
case Short i -> {
486+
return SHORT;
487+
}
488+
case ZonedDateTime zonedDateTime -> {
489+
return DATETIME;
490+
}
491+
case List<?> list -> {
492+
if (list.isEmpty()) {
493+
return null;
494+
}
495+
return fromJava(list.getFirst());
496+
}
497+
default -> {
498+
if (value instanceof String || value instanceof Character || value instanceof BytesRef) {
499+
return KEYWORD;
500+
}
501+
return null;
502+
}
491503
}
492504

493-
return null;
494505
}
495506

496507
public static boolean isUnsupported(DataType from) {

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -687,14 +687,125 @@ public void testErrorMessageForInvalidIntervalParams() throws IOException {
687687
);
688688
}
689689

690-
public void testErrorMessageForArrayValuesInParams() throws IOException {
690+
public void testArrayValuesAllowedInValueParams() throws IOException {
691+
Map<String, Object> responseMap = runEsql(
692+
RequestObjectBuilder.jsonBuilder()
693+
.query("row a = ?a | eval b = ?b, c = ?c, d = ?d, e = ?e")
694+
.params(
695+
"[{\"a\" : [\"a1\", \"a2\"]}, {\"b\" : [1, 2]}, {\"c\": [true, false]}, {\"d\": [1.1, 2.2]},"
696+
+ " {\"e\": [1674835275193, 1674835275193]}]"
697+
)
698+
);
699+
System.out.println(responseMap);
700+
701+
ListMatcher values = matchesList().item(
702+
matchesList().item(matchesList().item("a1").item("a2"))
703+
.item(matchesList().item(1).item(2))
704+
.item(matchesList().item(true).item(false))
705+
.item(matchesList().item(1.1).item(2.2))
706+
.item(matchesList().item(1674835275193L).item(1674835275193L))
707+
);
708+
}
709+
710+
public void testArrayValuesNullsNotAllowedInValueParams() throws IOException {
711+
ResponseException re = expectThrows(
712+
ResponseException.class,
713+
() -> runEsqlSync(
714+
RequestObjectBuilder.jsonBuilder()
715+
.query("row a = ?a | eval b = ?b, c = ?c, d = ?d, e = ?e, f = ?f")
716+
.params(
717+
"[{\"a\" : [null, \"a2\"]}, {\"b\" : [null, 2]}, {\"c\": [null, false]}, {\"d\": [null, 2.2]},"
718+
+ " {\"e\": [null, 1674835275193]}, {\"f\": [null, null]}]"
719+
)
720+
)
721+
);
722+
723+
String error = EntityUtils.toString(re.getResponse().getEntity()).replaceAll("\\\\\n\s+\\\\", "");
724+
assertThat(
725+
error,
726+
containsString(
727+
"[1:79] Parameter [a] contains a null entry: [null, a2]. " + "Null values are not allowed in multivalued params;"
728+
)
729+
);
730+
assertThat(
731+
error,
732+
containsString(
733+
"[1:101] Parameter [b] contains a null entry: [null, 2]. " + "Null values are not allowed in multivalued params;"
734+
)
735+
);
736+
assertThat(
737+
error,
738+
containsString(
739+
"[1:120] Parameter [c] contains a null entry: [null, false]. " + "Null values are not allowed in multivalued params;"
740+
)
741+
);
742+
assertThat(
743+
error,
744+
containsString(
745+
"[1:142] Parameter [d] contains a null entry: [null, 2.2]. " + "Null values are not allowed in multivalued params;"
746+
)
747+
);
748+
assertThat(
749+
error,
750+
containsString(
751+
"[1:162] Parameter [e] contains a null entry: [null, 1674835275193]. "
752+
+ "Null values are not allowed in multivalued params;"
753+
)
754+
);
755+
assertThat(
756+
error,
757+
containsString(
758+
"[1:192] Parameter [f] contains a null entry: [null, null]. " + "Null values are not allowed in multivalued params"
759+
)
760+
);
761+
}
762+
763+
public void testArrayValuesAllowedInUnnamedParams() throws IOException {
764+
Map<String, Object> responseMap = runEsql(
765+
RequestObjectBuilder.jsonBuilder()
766+
.query("row a = ? | eval b = ?, c = ?, d = ?, e = ?")
767+
.params("[[\"a1\", \"a2\"], [1, 2], [true, false], [1.1, 2.2], [1674835275193, 1674835275193]]")
768+
);
769+
System.out.println(responseMap);
770+
771+
ListMatcher values = matchesList().item(
772+
matchesList().item(matchesList().item("a1").item("a2"))
773+
.item(matchesList().item(1).item(2))
774+
.item(matchesList().item(true).item(false))
775+
.item(matchesList().item(1.1).item(2.2))
776+
.item(matchesList().item(1674835275193L).item(1674835275193L))
777+
);
778+
779+
assertResultMap(
780+
responseMap,
781+
matchesList().item(matchesMap().entry("name", "a").entry("type", "keyword"))
782+
.item(matchesMap().entry("name", "b").entry("type", "integer"))
783+
.item(matchesMap().entry("name", "c").entry("type", "boolean"))
784+
.item(matchesMap().entry("name", "d").entry("type", "double"))
785+
.item(matchesMap().entry("name", "e").entry("type", "long")),
786+
values
787+
);
788+
}
789+
790+
public void testErrorMessageForArrayValuesInNonValueParams() throws IOException {
691791
ResponseException re = expectThrows(
692792
ResponseException.class,
693-
() -> runEsql(RequestObjectBuilder.jsonBuilder().query("row a = 1 | eval x = ?").params("[{\"n1\": [5, 6, 7]}]"))
793+
() -> runEsql(
794+
RequestObjectBuilder.jsonBuilder().query("row a = 1 | eval ?n1 = 5").params("[{\"n1\" : {\"identifier\" : [\"integer\"]}}]")
795+
)
796+
);
797+
assertThat(
798+
EntityUtils.toString(re.getResponse().getEntity()),
799+
containsString("\"Failed to parse params: [1:47] n1={identifier=[integer]} parameter is multivalued")
800+
);
801+
802+
re = expectThrows(
803+
ResponseException.class,
804+
() -> runEsql(RequestObjectBuilder.jsonBuilder().query("row a = 1 | keep ?n1").params("[{\"n1\" : {\"pattern\" : [\"a*\"]}}]"))
694805
);
695806
assertThat(
696807
EntityUtils.toString(re.getResponse().getEntity()),
697-
containsString("Failed to parse params: [1:45] n1=[5, 6, 7] is not supported as a parameter")
808+
containsString("\"Failed to parse params: [1:43] n1={pattern=[a*]} parameter is multivalued")
698809
);
699810
}
700811

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
import org.elasticsearch.xpack.esql.VerificationException;
2323
import org.elasticsearch.xpack.esql.action.AbstractEsqlIntegTestCase;
2424
import org.elasticsearch.xpack.esql.action.EsqlCapabilities;
25+
import org.elasticsearch.xpack.esql.action.EsqlQueryRequest;
26+
import org.elasticsearch.xpack.esql.core.type.DataType;
27+
import org.elasticsearch.xpack.esql.parser.ParserUtils;
28+
import org.elasticsearch.xpack.esql.parser.QueryParam;
29+
import org.elasticsearch.xpack.esql.parser.QueryParams;
2530
import org.junit.Before;
2631

2732
import java.io.IOException;
@@ -130,6 +135,33 @@ public void testKnnOptions() {
130135
}
131136
}
132137

138+
public void testDenseVectorQueryParams() {
139+
float[] queryVector = new float[numDims];
140+
Arrays.fill(queryVector, 0);
141+
EsqlQueryRequest queryRequest = new EsqlQueryRequest();
142+
QueryParams queryParams = new QueryParams(
143+
List.of(new QueryParam("queryVector", Arrays.asList(queryVector), DataType.INTEGER, ParserUtils.ParamClassification.VALUE))
144+
);
145+
146+
queryRequest.params(queryParams);
147+
148+
var query = String.format(Locale.ROOT, """
149+
FROM test METADATA _score
150+
| WHERE knn(vector, %s) OR id > 100
151+
| KEEP id, _score, vector
152+
| SORT _score DESC
153+
| LIMIT 5
154+
""", Arrays.toString(queryVector));
155+
156+
try (var resp = run(query)) {
157+
assertColumnNames(resp.columns(), List.of("id", "_score", "vector"));
158+
assertColumnTypes(resp.columns(), List.of("integer", "double", "dense_vector"));
159+
160+
List<List<Object>> valuesList = EsqlTestUtils.getValuesList(resp);
161+
assertEquals(5, valuesList.size());
162+
}
163+
}
164+
133165
public void testKnnNonPushedDown() {
134166
float[] queryVector = new float[numDims];
135167
Arrays.fill(queryVector, 0.0f);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1477,7 +1477,12 @@ public enum Cap {
14771477
/**
14781478
* Support present_over_time aggregation that gets evaluated per time-series
14791479
*/
1480-
PRESENT_OVER_TIME(Build.current().isSnapshot());
1480+
PRESENT_OVER_TIME(Build.current().isSnapshot()),
1481+
1482+
/**
1483+
* Multivalued query parameters
1484+
*/
1485+
QUERY_PARAMS_MULTI_VALUES();
14811486

14821487
private final boolean enabled;
14831488

0 commit comments

Comments
 (0)