From a48c90992e330b2fcd6cec39f62400dbb648f25d Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 8 Jan 2024 14:29:36 -0800 Subject: [PATCH 1/5] Throw proper exception to invalid k-NN query Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + .../knn/index/query/KNNQueryBuilder.java | 6 +++ .../knn/index/VectorDataTypeIT.java | 45 +++++++++++++++++++ .../knn/index/query/KNNQueryBuilderTests.java | 37 +++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aafc2e585..2f52a1ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Allow nested knn field mapping when train model [#1318](https://github.com/opensearch-project/k-NN/pull/1318) * Properly designate model state for actively training models when nodes crash or leave cluster [#1317](https://github.com/opensearch-project/k-NN/pull/1317) * Fix script score queries not getting cached [#1367](https://github.com/opensearch-project/k-NN/pull/1367) +* Throw proper exception to invalid k-NN query [#1380](https://github.com/opensearch-project/k-NN/pull/1380) ### Infrastructure * Upgrade gradle to 8.4 [1289](https://github.com/opensearch-project/k-NN/pull/1289) * Refactor security testing to install from individual components [#1307](https://github.com/opensearch-project/k-NN/pull/1307) diff --git a/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java b/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java index 6caf2ed9b..48b30f3a7 100644 --- a/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java +++ b/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java @@ -100,8 +100,14 @@ public static void initialize(ModelDao modelDao) { } private static float[] ObjectsToFloats(List objs) { + if (Objects.isNull(objs)) { + throw new IllegalArgumentException("[" + NAME + "] requires 'vector' to be non-null"); + } float[] vec = new float[objs.size()]; for (int i = 0; i < objs.size(); i++) { + if (!(objs.get(i) instanceof Number)) { + throw new IllegalArgumentException("[" + NAME + "] requires 'vector' to be an array of numbers"); + } vec[i] = ((Number) objs.get(i)).floatValue(); } return vec; diff --git a/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java b/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java index 2d3f53580..ef669fa93 100644 --- a/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java +++ b/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java @@ -425,6 +425,51 @@ public void testKNNScriptScoreWithInvalidByteQueryVector() throws Exception { ); } + @SneakyThrows + public void testSearchWithInvalidSearchVectorType() { + createKnnIndexMappingWithLuceneEngine(2, SpaceType.L2, VectorDataType.FLOAT.getValue()); + ingestL2FloatTestData(); + Request request = new Request("POST", String.format("/%s/_search", INDEX_NAME)); + request.setJsonEntity( + "{\n" + + " \"query\": {\n" + + " \"knn\": {\n" + + " \"test-field-vec-dt\": {\n" + + " \"vector\": [\"a\", \"b\", \"c\", \"d\"],\n" + + " \"k\": 4\n" + + " }\n" + + " }\n" + + " }\n" + + "}" + ); + + ResponseException ex = expectThrows(ResponseException.class, () -> client().performRequest(request)); + assertTrue(ex.getResponse().getStatusLine().getReasonPhrase().contains("Bad Request")); + assertTrue(ex.getMessage().contains(String.format(Locale.ROOT, "[knn] requires 'vector' to be an array of numbers"))); + } + + @SneakyThrows + public void testSearchWithMissingQueryVector() { + createKnnIndexMappingWithLuceneEngine(2, SpaceType.L2, VectorDataType.FLOAT.getValue()); + ingestL2FloatTestData(); + Request request = new Request("POST", String.format("/%s/_search", INDEX_NAME)); + request.setJsonEntity( + "{\n" + + " \"query\": {\n" + + " \"knn\": {\n" + + " \"test-field-vec-dt\": {\n" + + " \"k\": 4\n" + + " }\n" + + " }\n" + + " }\n" + + "}" + ); + + ResponseException ex = expectThrows(ResponseException.class, () -> client().performRequest(request)); + assertTrue(ex.getResponse().getStatusLine().getReasonPhrase().contains("Bad Request")); + assertTrue(ex.getMessage().contains(String.format(Locale.ROOT, "[knn] requires 'vector' to be non-null"))); + } + @SneakyThrows private void ingestL2ByteTestData() { Byte[] b1 = { 6, 6 }; diff --git a/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java b/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java index e540725fc..e8ab48f34 100644 --- a/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java +++ b/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java @@ -127,6 +127,43 @@ public void testFromXcontent_WithFilter() throws Exception { actualBuilder.equals(knnQueryBuilder); } + public void testFromXContent_invalidQueryVectorType() throws Exception { + final ClusterService clusterService = mockClusterService(Version.CURRENT); + + final KNNClusterUtil knnClusterUtil = KNNClusterUtil.instance(); + knnClusterUtil.initialize(clusterService); + + String[] invalidTypeQueryVector = { "a", "b", "c", "d" }; + + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.startObject(FIELD_NAME); + builder.field(KNNQueryBuilder.VECTOR_FIELD.getPreferredName(), invalidTypeQueryVector); + builder.field(KNNQueryBuilder.K_FIELD.getPreferredName(), K); + builder.endObject(); + builder.endObject(); + XContentParser contentParser = createParser(builder); + contentParser.nextToken(); + expectThrows(IllegalArgumentException.class, () -> KNNQueryBuilder.fromXContent(contentParser)); + } + + public void testFromXContent_missingQueryVector() throws Exception { + final ClusterService clusterService = mockClusterService(Version.CURRENT); + + final KNNClusterUtil knnClusterUtil = KNNClusterUtil.instance(); + knnClusterUtil.initialize(clusterService); + + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.startObject(FIELD_NAME); + builder.field(KNNQueryBuilder.K_FIELD.getPreferredName(), K); + builder.endObject(); + builder.endObject(); + XContentParser contentParser = createParser(builder); + contentParser.nextToken(); + expectThrows(IllegalArgumentException.class, () -> KNNQueryBuilder.fromXContent(contentParser)); + } + @Override protected NamedXContentRegistry xContentRegistry() { List list = ClusterModule.getNamedXWriteables(); From 046d929cc7b6ab68f0d85317df40b1aa33bea559 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 8 Jan 2024 15:58:52 -0800 Subject: [PATCH 2/5] Move PR to enhancement in CHANGELOG.md Signed-off-by: Junqiu Lei --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f52a1ff7..b3bfdeccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Increase Lucene max dimension limit to 16,000 [#1346](https://github.com/opensearch-project/k-NN/pull/1346) * Tuned default values for ef_search and ef_construction for better indexing and search performance for vector search [#1353](https://github.com/opensearch-project/k-NN/pull/1353) * Enabled Filtering on Nested Vector fields with top level filters [#1372](https://github.com/opensearch-project/k-NN/pull/1372) +* * Throw proper exception to invalid k-NN query [#1380](https://github.com/opensearch-project/k-NN/pull/1380) ### Bug Fixes * Fix use-after-free case on nmslib search path [#1305](https://github.com/opensearch-project/k-NN/pull/1305) * Allow nested knn field mapping when train model [#1318](https://github.com/opensearch-project/k-NN/pull/1318) * Properly designate model state for actively training models when nodes crash or leave cluster [#1317](https://github.com/opensearch-project/k-NN/pull/1317) * Fix script score queries not getting cached [#1367](https://github.com/opensearch-project/k-NN/pull/1367) -* Throw proper exception to invalid k-NN query [#1380](https://github.com/opensearch-project/k-NN/pull/1380) ### Infrastructure * Upgrade gradle to 8.4 [1289](https://github.com/opensearch-project/k-NN/pull/1289) * Refactor security testing to install from individual components [#1307](https://github.com/opensearch-project/k-NN/pull/1307) From cf37f59d5a2967d0c9aea570d75491b96e48759a Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 8 Jan 2024 17:01:38 -0800 Subject: [PATCH 3/5] Resolve PR feedback Signed-off-by: Junqiu Lei --- CHANGELOG.md | 2 +- .../knn/index/query/KNNQueryBuilder.java | 4 +- .../knn/index/VectorDataTypeIT.java | 45 ------------------- .../knn/index/query/KNNQueryBuilderTests.java | 12 ++++- 4 files changed, 13 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3bfdeccd..a9c34eaf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Increase Lucene max dimension limit to 16,000 [#1346](https://github.com/opensearch-project/k-NN/pull/1346) * Tuned default values for ef_search and ef_construction for better indexing and search performance for vector search [#1353](https://github.com/opensearch-project/k-NN/pull/1353) * Enabled Filtering on Nested Vector fields with top level filters [#1372](https://github.com/opensearch-project/k-NN/pull/1372) -* * Throw proper exception to invalid k-NN query [#1380](https://github.com/opensearch-project/k-NN/pull/1380) +* Throw proper exception to invalid k-NN query [#1380](https://github.com/opensearch-project/k-NN/pull/1380) ### Bug Fixes * Fix use-after-free case on nmslib search path [#1305](https://github.com/opensearch-project/k-NN/pull/1305) * Allow nested knn field mapping when train model [#1318](https://github.com/opensearch-project/k-NN/pull/1318) diff --git a/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java b/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java index 48b30f3a7..0d0ce89b1 100644 --- a/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java +++ b/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java @@ -101,12 +101,12 @@ public static void initialize(ModelDao modelDao) { private static float[] ObjectsToFloats(List objs) { if (Objects.isNull(objs)) { - throw new IllegalArgumentException("[" + NAME + "] requires 'vector' to be non-null"); + throw new IllegalArgumentException(String.format("[%s] requires 'vector' to be non-null", NAME)); } float[] vec = new float[objs.size()]; for (int i = 0; i < objs.size(); i++) { if (!(objs.get(i) instanceof Number)) { - throw new IllegalArgumentException("[" + NAME + "] requires 'vector' to be an array of numbers"); + throw new IllegalArgumentException(String.format("[%s] requires 'vector' to be an array of numbers", NAME)); } vec[i] = ((Number) objs.get(i)).floatValue(); } diff --git a/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java b/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java index ef669fa93..2d3f53580 100644 --- a/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java +++ b/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java @@ -425,51 +425,6 @@ public void testKNNScriptScoreWithInvalidByteQueryVector() throws Exception { ); } - @SneakyThrows - public void testSearchWithInvalidSearchVectorType() { - createKnnIndexMappingWithLuceneEngine(2, SpaceType.L2, VectorDataType.FLOAT.getValue()); - ingestL2FloatTestData(); - Request request = new Request("POST", String.format("/%s/_search", INDEX_NAME)); - request.setJsonEntity( - "{\n" - + " \"query\": {\n" - + " \"knn\": {\n" - + " \"test-field-vec-dt\": {\n" - + " \"vector\": [\"a\", \"b\", \"c\", \"d\"],\n" - + " \"k\": 4\n" - + " }\n" - + " }\n" - + " }\n" - + "}" - ); - - ResponseException ex = expectThrows(ResponseException.class, () -> client().performRequest(request)); - assertTrue(ex.getResponse().getStatusLine().getReasonPhrase().contains("Bad Request")); - assertTrue(ex.getMessage().contains(String.format(Locale.ROOT, "[knn] requires 'vector' to be an array of numbers"))); - } - - @SneakyThrows - public void testSearchWithMissingQueryVector() { - createKnnIndexMappingWithLuceneEngine(2, SpaceType.L2, VectorDataType.FLOAT.getValue()); - ingestL2FloatTestData(); - Request request = new Request("POST", String.format("/%s/_search", INDEX_NAME)); - request.setJsonEntity( - "{\n" - + " \"query\": {\n" - + " \"knn\": {\n" - + " \"test-field-vec-dt\": {\n" - + " \"k\": 4\n" - + " }\n" - + " }\n" - + " }\n" - + "}" - ); - - ResponseException ex = expectThrows(ResponseException.class, () -> client().performRequest(request)); - assertTrue(ex.getResponse().getStatusLine().getReasonPhrase().contains("Bad Request")); - assertTrue(ex.getMessage().contains(String.format(Locale.ROOT, "[knn] requires 'vector' to be non-null"))); - } - @SneakyThrows private void ingestL2ByteTestData() { Byte[] b1 = { 6, 6 }; diff --git a/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java b/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java index e8ab48f34..d3e9c7108 100644 --- a/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java +++ b/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java @@ -144,7 +144,11 @@ public void testFromXContent_invalidQueryVectorType() throws Exception { builder.endObject(); XContentParser contentParser = createParser(builder); contentParser.nextToken(); - expectThrows(IllegalArgumentException.class, () -> KNNQueryBuilder.fromXContent(contentParser)); + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> KNNQueryBuilder.fromXContent(contentParser) + ); + assertTrue(exception.getMessage().contains("[knn] requires 'vector' to be an array of numbers")); } public void testFromXContent_missingQueryVector() throws Exception { @@ -161,7 +165,11 @@ public void testFromXContent_missingQueryVector() throws Exception { builder.endObject(); XContentParser contentParser = createParser(builder); contentParser.nextToken(); - expectThrows(IllegalArgumentException.class, () -> KNNQueryBuilder.fromXContent(contentParser)); + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> KNNQueryBuilder.fromXContent(contentParser) + ); + assertTrue(exception.getMessage().contains("[knn] requires 'vector' to be non-null")); } @Override From e500258970398a89a8b471d8f3b519979b456cf9 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 8 Jan 2024 18:14:28 -0800 Subject: [PATCH 4/5] Resolve PR feedback Signed-off-by: Junqiu Lei --- .../knn/index/query/KNNQueryBuilder.java | 8 ++-- .../knn/index/query/KNNQueryBuilderTests.java | 44 ++++++++++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java b/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java index 0d0ce89b1..096c2e30b 100644 --- a/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java +++ b/src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java @@ -100,13 +100,13 @@ public static void initialize(ModelDao modelDao) { } private static float[] ObjectsToFloats(List objs) { - if (Objects.isNull(objs)) { - throw new IllegalArgumentException(String.format("[%s] requires 'vector' to be non-null", NAME)); + if (Objects.isNull(objs) || objs.isEmpty()) { + throw new IllegalArgumentException(String.format("[%s] field 'vector' requires to be non-null and non-empty", NAME)); } float[] vec = new float[objs.size()]; for (int i = 0; i < objs.size(); i++) { - if (!(objs.get(i) instanceof Number)) { - throw new IllegalArgumentException(String.format("[%s] requires 'vector' to be an array of numbers", NAME)); + if ((objs.get(i) instanceof Number) == false) { + throw new IllegalArgumentException(String.format("[%s] field 'vector' requires to be an array of numbers", NAME)); } vec[i] = ((Number) objs.get(i)).floatValue(); } diff --git a/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java b/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java index d3e9c7108..a981c684e 100644 --- a/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java +++ b/src/test/java/org/opensearch/knn/index/query/KNNQueryBuilderTests.java @@ -39,6 +39,7 @@ import org.opensearch.plugins.SearchPlugin; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -133,7 +134,11 @@ public void testFromXContent_invalidQueryVectorType() throws Exception { final KNNClusterUtil knnClusterUtil = KNNClusterUtil.instance(); knnClusterUtil.initialize(clusterService); - String[] invalidTypeQueryVector = { "a", "b", "c", "d" }; + List invalidTypeQueryVector = new ArrayList<>(); + invalidTypeQueryVector.add(1.5); + invalidTypeQueryVector.add(2.5); + invalidTypeQueryVector.add("a"); + invalidTypeQueryVector.add(null); XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); @@ -148,7 +153,7 @@ public void testFromXContent_invalidQueryVectorType() throws Exception { IllegalArgumentException.class, () -> KNNQueryBuilder.fromXContent(contentParser) ); - assertTrue(exception.getMessage().contains("[knn] requires 'vector' to be an array of numbers")); + assertTrue(exception.getMessage().contains("[knn] field 'vector' requires to be an array of numbers")); } public void testFromXContent_missingQueryVector() throws Exception { @@ -157,19 +162,34 @@ public void testFromXContent_missingQueryVector() throws Exception { final KNNClusterUtil knnClusterUtil = KNNClusterUtil.instance(); knnClusterUtil.initialize(clusterService); - XContentBuilder builder = XContentFactory.jsonBuilder(); - builder.startObject(); - builder.startObject(FIELD_NAME); - builder.field(KNNQueryBuilder.K_FIELD.getPreferredName(), K); - builder.endObject(); - builder.endObject(); - XContentParser contentParser = createParser(builder); - contentParser.nextToken(); + // Test without vector field + XContentBuilder builderWithoutVectorField = XContentFactory.jsonBuilder(); + builderWithoutVectorField.startObject(); + builderWithoutVectorField.startObject(FIELD_NAME); + builderWithoutVectorField.field(KNNQueryBuilder.K_FIELD.getPreferredName(), K); + builderWithoutVectorField.endObject(); + builderWithoutVectorField.endObject(); + XContentParser contentParserWithoutVectorField = createParser(builderWithoutVectorField); + contentParserWithoutVectorField.nextToken(); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, - () -> KNNQueryBuilder.fromXContent(contentParser) + () -> KNNQueryBuilder.fromXContent(contentParserWithoutVectorField) ); - assertTrue(exception.getMessage().contains("[knn] requires 'vector' to be non-null")); + assertTrue(exception.getMessage().contains("[knn] field 'vector' requires to be non-null and non-empty")); + + // Test empty vector field + List emptyQueryVector = new ArrayList<>(); + XContentBuilder builderWithEmptyVector = XContentFactory.jsonBuilder(); + builderWithEmptyVector.startObject(); + builderWithEmptyVector.startObject(FIELD_NAME); + builderWithEmptyVector.field(KNNQueryBuilder.VECTOR_FIELD.getPreferredName(), emptyQueryVector); + builderWithEmptyVector.field(KNNQueryBuilder.K_FIELD.getPreferredName(), K); + builderWithEmptyVector.endObject(); + builderWithEmptyVector.endObject(); + XContentParser contentParserWithEmptyVector = createParser(builderWithEmptyVector); + contentParserWithEmptyVector.nextToken(); + exception = expectThrows(IllegalArgumentException.class, () -> KNNQueryBuilder.fromXContent(contentParserWithEmptyVector)); + assertTrue(exception.getMessage().contains("[knn] field 'vector' requires to be non-null and non-empty")); } @Override From 76e768021e9fec2381837b5146c4648ca9a15ccc Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 8 Jan 2024 18:27:55 -0800 Subject: [PATCH 5/5] Revert IT tests Signed-off-by: Junqiu Lei --- .../knn/index/VectorDataTypeIT.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java b/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java index 2d3f53580..e55a3be42 100644 --- a/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java +++ b/src/test/java/org/opensearch/knn/index/VectorDataTypeIT.java @@ -24,6 +24,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.script.Script; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -425,6 +426,56 @@ public void testKNNScriptScoreWithInvalidByteQueryVector() throws Exception { ); } + @SneakyThrows + public void testSearchWithInvalidSearchVectorType() { + createKnnIndexMappingWithLuceneEngine(2, SpaceType.L2, VectorDataType.FLOAT.getValue()); + ingestL2FloatTestData(); + Request request = new Request("POST", String.format("/%s/_search", INDEX_NAME)); + List invalidTypeQueryVector = new ArrayList<>(); + invalidTypeQueryVector.add(1.5); + invalidTypeQueryVector.add(2.5); + invalidTypeQueryVector.add("a"); + invalidTypeQueryVector.add(null); + XContentBuilder builder = XContentFactory.jsonBuilder() + .startObject() + .startObject("query") + .startObject("knn") + .startObject(FIELD_NAME) + .field("vector", invalidTypeQueryVector) + .field("k", 4) + .endObject() + .endObject() + .endObject() + .endObject(); + request.setJsonEntity(builder.toString()); + + ResponseException ex = expectThrows(ResponseException.class, () -> client().performRequest(request)); + assertEquals(400, ex.getResponse().getStatusLine().getStatusCode()); + assertTrue(ex.getMessage().contains("[knn] field 'vector' requires to be an array of numbers")); + } + + @SneakyThrows + public void testSearchWithMissingQueryVector() { + createKnnIndexMappingWithLuceneEngine(2, SpaceType.L2, VectorDataType.FLOAT.getValue()); + ingestL2FloatTestData(); + Request request = new Request("POST", String.format("/%s/_search", INDEX_NAME)); + XContentBuilder builder = XContentFactory.jsonBuilder() + .startObject() + .startObject("query") + .startObject("knn") + .startObject(FIELD_NAME) + .field("k", 4) + .endObject() + .endObject() + .endObject() + .endObject(); + request.setJsonEntity(builder.toString()); + + ResponseException ex = expectThrows(ResponseException.class, () -> client().performRequest(request)); + assertEquals(400, ex.getResponse().getStatusLine().getStatusCode()); + assertTrue(ex.getMessage().contains("[knn] field 'vector' requires to be non-null and non-empty")); + } + @SneakyThrows private void ingestL2ByteTestData() { Byte[] b1 = { 6, 6 };