From d7a9c50cf24abe11c76a14da4c6e304480eea728 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:32:09 +1100 Subject: [PATCH 01/39] Mute org.elasticsearch.packaging.test.ArchiveTests test60StartAndStop #118216 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index a36723469189..4523db7239be 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -294,6 +294,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118220 - class: org.elasticsearch.validation.DotPrefixClientYamlTestSuiteIT issue: https://github.com/elastic/elasticsearch/issues/118224 +- class: org.elasticsearch.packaging.test.ArchiveTests + method: test60StartAndStop + issue: https://github.com/elastic/elasticsearch/issues/118216 # Examples: # From b2b8e3f762753ac903f056a4238a204e993bb8d5 Mon Sep 17 00:00:00 2001 From: kosabogi <105062005+kosabogi@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:05:11 +0100 Subject: [PATCH 02/39] [DOCS] [8.17] Adds new default inference endpoint information (#117985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds new default inference information * Update docs/reference/mapping/types/semantic-text.asciidoc Co-authored-by: István Zoltán Szabó * Update docs/reference/search/search-your-data/semantic-search-semantic-text.asciidoc Co-authored-by: István Zoltán Szabó * Update docs/reference/mapping/types/semantic-text.asciidoc Co-authored-by: David Kyle --------- Co-authored-by: István Zoltán Szabó Co-authored-by: David Kyle --- .../mapping/types/semantic-text.asciidoc | 19 +++---- .../semantic-search-semantic-text.asciidoc | 8 +-- .../semantic-text-hybrid-search | 51 +++---------------- 3 files changed, 21 insertions(+), 57 deletions(-) diff --git a/docs/reference/mapping/types/semantic-text.asciidoc b/docs/reference/mapping/types/semantic-text.asciidoc index b3e103ec6dbd..96dc402e10c6 100644 --- a/docs/reference/mapping/types/semantic-text.asciidoc +++ b/docs/reference/mapping/types/semantic-text.asciidoc @@ -12,13 +12,14 @@ Long passages are <> to smaller secti The `semantic_text` field type specifies an inference endpoint identifier that will be used to generate embeddings. You can create the inference endpoint by using the <>. -This field type and the <> type make it simpler to perform semantic search on your data. -If you don't specify an inference endpoint, the <> is used by default. +This field type and the <> type make it simpler to perform semantic search on your data. + +If you don’t specify an inference endpoint, the `inference_id` field defaults to `.elser-2-elasticsearch`, a preconfigured endpoint for the elasticsearch service. Using `semantic_text`, you won't need to specify how to generate embeddings for your data, or how to index it. The {infer} endpoint automatically determines the embedding generation, indexing, and query to use. -If you use the ELSER service, you can set up `semantic_text` with the following API request: +If you use the preconfigured `.elser-2-elasticsearch` endpoint, you can set up `semantic_text` with the following API request: [source,console] ------------------------------------------------------------ @@ -34,7 +35,7 @@ PUT my-index-000001 } ------------------------------------------------------------ -If you use a service other than ELSER, you must create an {infer} endpoint using the <> and reference it when setting up `semantic_text` as the following example demonstrates: +To use a custom {infer} endpoint instead of the default `.elser-2-elasticsearch`, you must <> and specify its `inference_id` when setting up the `semantic_text` field type. [source,console] ------------------------------------------------------------ @@ -53,8 +54,7 @@ PUT my-index-000002 // TEST[skip:Requires inference endpoint] <1> The `inference_id` of the {infer} endpoint to use to generate embeddings. - -The recommended way to use semantic_text is by having dedicated {infer} endpoints for ingestion and search. +The recommended way to use `semantic_text` is by having dedicated {infer} endpoints for ingestion and search. This ensures that search speed remains unaffected by ingestion workloads, and vice versa. After creating dedicated {infer} endpoints for both, you can reference them using the `inference_id` and `search_inference_id` parameters when setting up the index mapping for an index that uses the `semantic_text` field. @@ -82,10 +82,11 @@ PUT my-index-000003 `inference_id`:: (Required, string) -{infer-cap} endpoint that will be used to generate the embeddings for the field. +{infer-cap} endpoint that will be used to generate embeddings for the field. +By default, `.elser-2-elasticsearch` is used. This parameter cannot be updated. Use the <> to create the endpoint. -If `search_inference_id` is specified, the {infer} endpoint defined by `inference_id` will only be used at index time. +If `search_inference_id` is specified, the {infer} endpoint will only be used at index time. `search_inference_id`:: (Optional, string) @@ -201,7 +202,7 @@ PUT test-index "properties": { "infer_field": { "type": "semantic_text", - "inference_id": "my-elser-endpoint" + "inference_id": ".elser-2-elasticsearch" }, "source_field": { "type": "text", diff --git a/docs/reference/search/search-your-data/semantic-search-semantic-text.asciidoc b/docs/reference/search/search-your-data/semantic-search-semantic-text.asciidoc index ba9c81db2138..3448940b6fad 100644 --- a/docs/reference/search/search-your-data/semantic-search-semantic-text.asciidoc +++ b/docs/reference/search/search-your-data/semantic-search-semantic-text.asciidoc @@ -14,15 +14,15 @@ You don't need to define model related settings and parameters, or create {infer The recommended way to use <> in the {stack} is following the `semantic_text` workflow. When you need more control over indexing and query settings, you can still use the complete {infer} workflow (refer to <> to review the process). -This tutorial uses the <> for demonstration, but you can use any service and their supported models offered by the {infer-cap} API. +This tutorial uses the <> for demonstration, but you can use any service and their supported models offered by the {infer-cap} API. [discrete] [[semantic-text-requirements]] ==== Requirements -This tutorial uses the <> for demonstration, which is created automatically as needed. -To use the `semantic_text` field type with an {infer} service other than ELSER, you must create an inference endpoint using the <>. +This tutorial uses the <> for demonstration, which is created automatically as needed. +To use the `semantic_text` field type with an {infer} service other than `elasticsearch` service, you must create an inference endpoint using the <>. [discrete] @@ -48,7 +48,7 @@ PUT semantic-embeddings // TEST[skip:TBD] <1> The name of the field to contain the generated embeddings. <2> The field to contain the embeddings is a `semantic_text` field. -Since no `inference_id` is provided, the <> is used by default. +Since no `inference_id` is provided, the default endpoint `.elser-2-elasticsearch` for the <> is used. To use a different {infer} service, you must create an {infer} endpoint first using the <> and then specify it in the `semantic_text` field mapping using the `inference_id` parameter. [NOTE] diff --git a/docs/reference/search/search-your-data/semantic-text-hybrid-search b/docs/reference/search/search-your-data/semantic-text-hybrid-search index c56b283434df..4b49a7c3155d 100644 --- a/docs/reference/search/search-your-data/semantic-text-hybrid-search +++ b/docs/reference/search/search-your-data/semantic-text-hybrid-search @@ -8,47 +8,12 @@ This tutorial demonstrates how to perform hybrid search, combining semantic sear In hybrid search, semantic search retrieves results based on the meaning of the text, while full-text search focuses on exact word matches. By combining both methods, hybrid search delivers more relevant results, particularly in cases where relying on a single approach may not be sufficient. -The recommended way to use hybrid search in the {stack} is following the `semantic_text` workflow. This tutorial uses the <> for demonstration, but you can use any service and its supported models offered by the {infer-cap} API. - -[discrete] -[[semantic-text-hybrid-infer-endpoint]] -==== Create the {infer} endpoint - -Create an inference endpoint by using the <>: - -[source,console] ------------------------------------------------------------- -PUT _inference/sparse_embedding/my-elser-endpoint <1> -{ - "service": "elser", <2> - "service_settings": { - "adaptive_allocations": { <3> - "enabled": true, - "min_number_of_allocations": 3, - "max_number_of_allocations": 10 - }, - "num_threads": 1 - } -} ------------------------------------------------------------- -// TEST[skip:TBD] -<1> The task type is `sparse_embedding` in the path as the `elser` service will -be used and ELSER creates sparse vectors. The `inference_id` is -`my-elser-endpoint`. -<2> The `elser` service is used in this example. -<3> This setting enables and configures adaptive allocations. -Adaptive allocations make it possible for ELSER to automatically scale up or down resources based on the current load on the process. - -[NOTE] -==== -You might see a 502 bad gateway error in the response when using the {kib} Console. -This error usually just reflects a timeout, while the model downloads in the background. -You can check the download progress in the {ml-app} UI. -==== +The recommended way to use hybrid search in the {stack} is following the `semantic_text` workflow. +This tutorial uses the <> for demonstration, but you can use any service and their supported models offered by the {infer-cap} API. [discrete] [[hybrid-search-create-index-mapping]] -==== Create an index mapping for hybrid search +==== Create an index mapping The destination index will contain both the embeddings for semantic search and the original text field for full-text search. This structure enables the combination of semantic search and full-text search. @@ -60,11 +25,10 @@ PUT semantic-embeddings "properties": { "semantic_text": { <1> "type": "semantic_text", - "inference_id": "my-elser-endpoint" <2> }, - "content": { <3> + "content": { <2> "type": "text", - "copy_to": "semantic_text" <4> + "copy_to": "semantic_text" <3> } } } @@ -72,9 +36,8 @@ PUT semantic-embeddings ------------------------------------------------------------ // TEST[skip:TBD] <1> The name of the field to contain the generated embeddings for semantic search. -<2> The identifier of the inference endpoint that generates the embeddings based on the input text. -<3> The name of the field to contain the original text for lexical search. -<4> The textual data stored in the `content` field will be copied to `semantic_text` and processed by the {infer} endpoint. +<2> The name of the field to contain the original text for lexical search. +<3> The textual data stored in the `content` field will be copied to `semantic_text` and processed by the {infer} endpoint. [NOTE] ==== From 7ffac3b3f3da18e46af023341f912b83899f0863 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 9 Dec 2024 09:08:01 +0100 Subject: [PATCH 03/39] Save O(1s) of CPU time in FieldSortIT (#118146) I see this `toString` take ~2s of hot CPU time in some test runs which isn't entirely surprising. Rather than optimize this in some form, just dropping the string here which aligns the thing with other tests anyway. --- .../java/org/elasticsearch/search/sort/FieldSortIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/sort/FieldSortIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/sort/FieldSortIT.java index 87665c3d784f..bf7a315040ca 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/sort/FieldSortIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/sort/FieldSortIT.java @@ -202,7 +202,6 @@ public void testIssue6614() throws InterruptedException { response -> { for (int j = 0; j < response.getHits().getHits().length; j++) { assertThat( - response.toString() + "\n vs. \n" + allDocsResponse.toString(), response.getHits().getHits()[j].getId(), equalTo(allDocsResponse.getHits().getHits()[j].getId()) ); From 22e8f61db903473b462bff9a8919b399d742162d Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Mon, 9 Dec 2024 10:36:27 +0100 Subject: [PATCH 04/39] Unmute test around can match shards skipping against searchable snapshots (#118189) This test has been muted for a long time. The failure may or may no longer be relevant. This commit unmutes it. If it fails again, we'll get updated info and look into it. Closes #105339 --- .../SearchableSnapshotsCanMatchOnCoordinatorIntegTests.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsCanMatchOnCoordinatorIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsCanMatchOnCoordinatorIntegTests.java index d4bbd4495df2..23e414c0dc1b 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsCanMatchOnCoordinatorIntegTests.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsCanMatchOnCoordinatorIntegTests.java @@ -42,7 +42,6 @@ import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.NodeRoles; -import org.elasticsearch.test.junit.annotations.TestIssueLogging; import org.elasticsearch.test.transport.MockTransportService; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotAction; @@ -788,11 +787,6 @@ public void testQueryPhaseIsExecutedInAnAvailableNodeWhenAllShardsCanBeSkipped() * Can match against searchable snapshots is tested via both the Search API and the SearchShards (transport-only) API. * The latter is a way to do only a can-match rather than all search phases. */ - @TestIssueLogging( - issueUrl = "https://github.com/elastic/elasticsearch/issues/97878", - value = "org.elasticsearch.snapshots:DEBUG,org.elasticsearch.indices.recovery:DEBUG,org.elasticsearch.action.search:DEBUG" - ) - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105339") public void testSearchableSnapshotShardsThatHaveMatchingDataAreNotSkippedOnTheCoordinatingNode() throws Exception { internalCluster().startMasterOnlyNode(); internalCluster().startCoordinatingOnlyNode(Settings.EMPTY); From 638e5b6de2daa785ba07548feb907ad9079a6c94 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 9 Dec 2024 09:59:53 +0000 Subject: [PATCH 05/39] Revert "Adding default endpoint for Elastic Rerank (#117939)" (#118221) This reverts commit 54c320ebc9b262e66ab92af660a8a155311059d4. --- docs/changelog/117939.yaml | 5 -- .../xpack/inference/DefaultEndPointsIT.java | 40 -------------- .../inference/InferenceBaseRestTest.java | 47 ++++------------ .../xpack/inference/InferenceCrudIT.java | 4 +- .../InferenceNamedWriteablesProvider.java | 6 +- .../elasticsearch/CustomElandRerankModel.java | 4 +- ...ava => CustomElandRerankTaskSettings.java} | 25 +++++---- .../elasticsearch/ElasticRerankerModel.java | 5 +- .../ElasticsearchInternalService.java | 55 ++++++------------- ...> CustomElandRerankTaskSettingsTests.java} | 48 ++++++++-------- .../ElasticsearchInternalServiceTests.java | 42 +++++++------- 11 files changed, 96 insertions(+), 185 deletions(-) delete mode 100644 docs/changelog/117939.yaml rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/{RerankTaskSettings.java => CustomElandRerankTaskSettings.java} (79%) rename x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/{RerankTaskSettingsTests.java => CustomElandRerankTaskSettingsTests.java} (53%) diff --git a/docs/changelog/117939.yaml b/docs/changelog/117939.yaml deleted file mode 100644 index d41111f099f9..000000000000 --- a/docs/changelog/117939.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 117939 -summary: Adding default endpoint for Elastic Rerank -area: Machine Learning -type: enhancement -issues: [] diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java index 068b3e1f4ce0..ba3e48e11928 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java @@ -57,9 +57,6 @@ public void testGet() throws IOException { var e5Model = getModel(ElasticsearchInternalService.DEFAULT_E5_ID); assertDefaultE5Config(e5Model); - - var rerankModel = getModel(ElasticsearchInternalService.DEFAULT_RERANK_ID); - assertDefaultRerankConfig(rerankModel); } @SuppressWarnings("unchecked") @@ -128,42 +125,6 @@ private static void assertDefaultE5Config(Map modelConfig) { assertDefaultChunkingSettings(modelConfig); } - @SuppressWarnings("unchecked") - public void testInferDeploysDefaultRerank() throws IOException { - var model = getModel(ElasticsearchInternalService.DEFAULT_RERANK_ID); - assertDefaultRerankConfig(model); - - var inputs = List.of("Hello World", "Goodnight moon"); - var query = "but why"; - var queryParams = Map.of("timeout", "120s"); - var results = infer(ElasticsearchInternalService.DEFAULT_RERANK_ID, TaskType.RERANK, inputs, query, queryParams); - var embeddings = (List>) results.get("rerank"); - assertThat(results.toString(), embeddings, hasSize(2)); - } - - @SuppressWarnings("unchecked") - private static void assertDefaultRerankConfig(Map modelConfig) { - assertEquals(modelConfig.toString(), ElasticsearchInternalService.DEFAULT_RERANK_ID, modelConfig.get("inference_id")); - assertEquals(modelConfig.toString(), ElasticsearchInternalService.NAME, modelConfig.get("service")); - assertEquals(modelConfig.toString(), TaskType.RERANK.toString(), modelConfig.get("task_type")); - - var serviceSettings = (Map) modelConfig.get("service_settings"); - assertThat(modelConfig.toString(), serviceSettings.get("model_id"), is(".rerank-v1")); - assertEquals(modelConfig.toString(), 1, serviceSettings.get("num_threads")); - - var adaptiveAllocations = (Map) serviceSettings.get("adaptive_allocations"); - assertThat( - modelConfig.toString(), - adaptiveAllocations, - Matchers.is(Map.of("enabled", true, "min_number_of_allocations", 0, "max_number_of_allocations", 32)) - ); - - var chunkingSettings = (Map) modelConfig.get("chunking_settings"); - assertNull(chunkingSettings); - var taskSettings = (Map) modelConfig.get("task_settings"); - assertThat(modelConfig.toString(), taskSettings, Matchers.is(Map.of("return_documents", true))); - } - @SuppressWarnings("unchecked") private static void assertDefaultChunkingSettings(Map modelConfig) { var chunkingSettings = (Map) modelConfig.get("chunking_settings"); @@ -198,7 +159,6 @@ public void onFailure(Exception exception) { var request = createInferenceRequest( Strings.format("_inference/%s", ElasticsearchInternalService.DEFAULT_ELSER_ID), inputs, - null, queryParams ); client().performRequestAsync(request, listener); diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java index 1716057cdfe4..07ce2fe00642 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java @@ -336,7 +336,7 @@ private List getInternalAsList(String endpoint) throws IOException { protected Map infer(String modelId, List input) throws IOException { var endpoint = Strings.format("_inference/%s", modelId); - return inferInternal(endpoint, input, null, Map.of()); + return inferInternal(endpoint, input, Map.of()); } protected Deque streamInferOnMockService(String modelId, TaskType taskType, List input) throws Exception { @@ -352,7 +352,7 @@ protected Deque unifiedCompletionInferOnMockService(String mode private Deque callAsync(String endpoint, List input) throws Exception { var request = new Request("POST", endpoint); - request.setJsonEntity(jsonBody(input, null)); + request.setJsonEntity(jsonBody(input)); return execAsyncCall(request); } @@ -394,60 +394,33 @@ private String createUnifiedJsonBody(List input, String role) throws IOE protected Map infer(String modelId, TaskType taskType, List input) throws IOException { var endpoint = Strings.format("_inference/%s/%s", taskType, modelId); - return inferInternal(endpoint, input, null, Map.of()); + return inferInternal(endpoint, input, Map.of()); } protected Map infer(String modelId, TaskType taskType, List input, Map queryParameters) throws IOException { var endpoint = Strings.format("_inference/%s/%s?error_trace", taskType, modelId); - return inferInternal(endpoint, input, null, queryParameters); + return inferInternal(endpoint, input, queryParameters); } - protected Map infer( - String modelId, - TaskType taskType, - List input, - String query, - Map queryParameters - ) throws IOException { - var endpoint = Strings.format("_inference/%s/%s?error_trace", taskType, modelId); - return inferInternal(endpoint, input, query, queryParameters); - } - - protected Request createInferenceRequest( - String endpoint, - List input, - @Nullable String query, - Map queryParameters - ) { + protected Request createInferenceRequest(String endpoint, List input, Map queryParameters) { var request = new Request("POST", endpoint); - request.setJsonEntity(jsonBody(input, query)); + request.setJsonEntity(jsonBody(input)); if (queryParameters.isEmpty() == false) { request.addParameters(queryParameters); } return request; } - private Map inferInternal( - String endpoint, - List input, - @Nullable String query, - Map queryParameters - ) throws IOException { - var request = createInferenceRequest(endpoint, input, query, queryParameters); + private Map inferInternal(String endpoint, List input, Map queryParameters) throws IOException { + var request = createInferenceRequest(endpoint, input, queryParameters); var response = client().performRequest(request); assertOkOrCreated(response); return entityAsMap(response); } - private String jsonBody(List input, @Nullable String query) { - final StringBuilder bodyBuilder = new StringBuilder("{"); - - if (query != null) { - bodyBuilder.append("\"query\":\"").append(query).append("\","); - } - - bodyBuilder.append("\"input\": ["); + private String jsonBody(List input) { + var bodyBuilder = new StringBuilder("{\"input\": ["); for (var in : input) { bodyBuilder.append('"').append(in).append('"').append(','); } diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java index 2099ec8287a7..1e19491aeaa6 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java @@ -49,7 +49,7 @@ public void testCRUD() throws IOException { } var getAllModels = getAllModels(); - int numModels = 12; + int numModels = 11; assertThat(getAllModels, hasSize(numModels)); var getSparseModels = getModels("_all", TaskType.SPARSE_EMBEDDING); @@ -537,7 +537,7 @@ private static String expectedResult(String input) { } public void testGetZeroModels() throws IOException { - var models = getModels("_all", TaskType.COMPLETION); + var models = getModels("_all", TaskType.RERANK); assertThat(models, empty()); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java index a4187f4c4fa9..b83c098ca808 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java @@ -63,12 +63,12 @@ import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSparseEmbeddingsServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandInternalTextEmbeddingServiceSettings; +import org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandRerankTaskSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticRerankerServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElserInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElserMlNodeTaskSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.MultilingualE5SmallInternalServiceSettings; -import org.elasticsearch.xpack.inference.services.elasticsearch.RerankTaskSettings; import org.elasticsearch.xpack.inference.services.googleaistudio.completion.GoogleAiStudioCompletionServiceSettings; import org.elasticsearch.xpack.inference.services.googleaistudio.embeddings.GoogleAiStudioEmbeddingsServiceSettings; import org.elasticsearch.xpack.inference.services.googlevertexai.GoogleVertexAiSecretSettings; @@ -518,7 +518,9 @@ private static void addCustomElandWriteables(final List namedWriteables) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java index 6388bb33bb78..f620b15680c8 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java @@ -17,7 +17,7 @@ import java.util.HashMap; import java.util.Map; -import static org.elasticsearch.xpack.inference.services.elasticsearch.RerankTaskSettings.RETURN_DOCUMENTS; +import static org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandRerankTaskSettings.RETURN_DOCUMENTS; public class CustomElandRerankModel extends CustomElandModel { @@ -26,7 +26,7 @@ public CustomElandRerankModel( TaskType taskType, String service, CustomElandInternalServiceSettings serviceSettings, - RerankTaskSettings taskSettings + CustomElandRerankTaskSettings taskSettings ) { super(inferenceEntityId, taskType, service, serviceSettings, taskSettings); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettings.java similarity index 79% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettings.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettings.java index 3c25f7a6a901..a0be1661b860 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettings.java @@ -26,14 +26,14 @@ /** * Defines the task settings for internal rerank service. */ -public class RerankTaskSettings implements TaskSettings { +public class CustomElandRerankTaskSettings implements TaskSettings { public static final String NAME = "custom_eland_rerank_task_settings"; public static final String RETURN_DOCUMENTS = "return_documents"; - static final RerankTaskSettings DEFAULT_SETTINGS = new RerankTaskSettings(Boolean.TRUE); + static final CustomElandRerankTaskSettings DEFAULT_SETTINGS = new CustomElandRerankTaskSettings(Boolean.TRUE); - public static RerankTaskSettings defaultsFromMap(Map map) { + public static CustomElandRerankTaskSettings defaultsFromMap(Map map) { ValidationException validationException = new ValidationException(); if (map == null || map.isEmpty()) { @@ -49,7 +49,7 @@ public static RerankTaskSettings defaultsFromMap(Map map) { returnDocuments = true; } - return new RerankTaskSettings(returnDocuments); + return new CustomElandRerankTaskSettings(returnDocuments); } /** @@ -57,13 +57,13 @@ public static RerankTaskSettings defaultsFromMap(Map map) { * @param map source map * @return Task settings */ - public static RerankTaskSettings fromMap(Map map) { + public static CustomElandRerankTaskSettings fromMap(Map map) { if (map == null || map.isEmpty()) { return DEFAULT_SETTINGS; } Boolean returnDocuments = extractOptionalBoolean(map, RETURN_DOCUMENTS, new ValidationException()); - return new RerankTaskSettings(returnDocuments); + return new CustomElandRerankTaskSettings(returnDocuments); } /** @@ -74,17 +74,20 @@ public static RerankTaskSettings fromMap(Map map) { * @param requestTaskSettings the settings passed in within the task_settings field of the request * @return Either {@code originalSettings} or {@code requestTaskSettings} */ - public static RerankTaskSettings of(RerankTaskSettings originalSettings, RerankTaskSettings requestTaskSettings) { + public static CustomElandRerankTaskSettings of( + CustomElandRerankTaskSettings originalSettings, + CustomElandRerankTaskSettings requestTaskSettings + ) { return requestTaskSettings.returnDocuments() != null ? requestTaskSettings : originalSettings; } private final Boolean returnDocuments; - public RerankTaskSettings(StreamInput in) throws IOException { + public CustomElandRerankTaskSettings(StreamInput in) throws IOException { this(in.readOptionalBoolean()); } - public RerankTaskSettings(@Nullable Boolean doReturnDocuments) { + public CustomElandRerankTaskSettings(@Nullable Boolean doReturnDocuments) { if (doReturnDocuments == null) { this.returnDocuments = true; } else { @@ -130,7 +133,7 @@ public Boolean returnDocuments() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - RerankTaskSettings that = (RerankTaskSettings) o; + CustomElandRerankTaskSettings that = (CustomElandRerankTaskSettings) o; return Objects.equals(returnDocuments, that.returnDocuments); } @@ -141,7 +144,7 @@ public int hashCode() { @Override public TaskSettings updatedTaskSettings(Map newSettings) { - RerankTaskSettings updatedSettings = RerankTaskSettings.fromMap(new HashMap<>(newSettings)); + CustomElandRerankTaskSettings updatedSettings = CustomElandRerankTaskSettings.fromMap(new HashMap<>(newSettings)); return of(this, updatedSettings); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java index 276bce6dbe8f..115cc9f05599 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java @@ -9,6 +9,7 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.inference.ChunkingSettings; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.TaskType; import org.elasticsearch.xpack.core.ml.action.CreateTrainedModelAssignmentAction; @@ -21,9 +22,9 @@ public ElasticRerankerModel( TaskType taskType, String service, ElasticRerankerServiceSettings serviceSettings, - RerankTaskSettings taskSettings + ChunkingSettings chunkingSettings ) { - super(inferenceEntityId, taskType, service, serviceSettings, taskSettings); + super(inferenceEntityId, taskType, service, serviceSettings, chunkingSettings); } @Override diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java index 5f613d6be586..8cb91782e238 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java @@ -103,7 +103,6 @@ public class ElasticsearchInternalService extends BaseElasticsearchInternalServi public static final int EMBEDDING_MAX_BATCH_SIZE = 10; public static final String DEFAULT_ELSER_ID = ".elser-2-elasticsearch"; public static final String DEFAULT_E5_ID = ".multilingual-e5-small-elasticsearch"; - public static final String DEFAULT_RERANK_ID = ".rerank-v1-elasticsearch"; private static final EnumSet supportedTaskTypes = EnumSet.of( TaskType.RERANK, @@ -228,7 +227,7 @@ public void parseRequestConfig( ) ); } else if (RERANKER_ID.equals(modelId)) { - rerankerCase(inferenceEntityId, taskType, config, serviceSettingsMap, taskSettingsMap, modelListener); + rerankerCase(inferenceEntityId, taskType, config, serviceSettingsMap, chunkingSettings, modelListener); } else { customElandCase(inferenceEntityId, taskType, serviceSettingsMap, taskSettingsMap, chunkingSettings, modelListener); } @@ -311,7 +310,7 @@ private static CustomElandModel createCustomElandModel( taskType, NAME, elandServiceSettings(serviceSettings, context), - RerankTaskSettings.fromMap(taskSettings) + CustomElandRerankTaskSettings.fromMap(taskSettings) ); default -> throw new ElasticsearchStatusException(TaskType.unsupportedTaskTypeErrorMsg(taskType, NAME), RestStatus.BAD_REQUEST); }; @@ -334,7 +333,7 @@ private void rerankerCase( TaskType taskType, Map config, Map serviceSettingsMap, - Map taskSettingsMap, + ChunkingSettings chunkingSettings, ActionListener modelListener ) { @@ -349,7 +348,7 @@ private void rerankerCase( taskType, NAME, new ElasticRerankerServiceSettings(esServiceSettingsBuilder.build()), - RerankTaskSettings.fromMap(taskSettingsMap) + chunkingSettings ) ); } @@ -515,14 +514,6 @@ public Model parsePersistedConfig(String inferenceEntityId, TaskType taskType, M ElserMlNodeTaskSettings.DEFAULT, chunkingSettings ); - } else if (modelId.equals(RERANKER_ID)) { - return new ElasticRerankerModel( - inferenceEntityId, - taskType, - NAME, - new ElasticRerankerServiceSettings(ElasticsearchInternalServiceSettings.fromPersistedMap(serviceSettingsMap)), - RerankTaskSettings.fromMap(taskSettingsMap) - ); } else { return createCustomElandModel( inferenceEntityId, @@ -674,23 +665,21 @@ public void inferRerank( ) { var request = buildInferenceRequest(model.mlNodeDeploymentId(), new TextSimilarityConfigUpdate(query), inputs, inputType, timeout); - var returnDocs = Boolean.TRUE; - if (model.getTaskSettings() instanceof RerankTaskSettings modelSettings) { - var requestSettings = RerankTaskSettings.fromMap(requestTaskSettings); - returnDocs = RerankTaskSettings.of(modelSettings, requestSettings).returnDocuments(); - } + var modelSettings = (CustomElandRerankTaskSettings) model.getTaskSettings(); + var requestSettings = CustomElandRerankTaskSettings.fromMap(requestTaskSettings); + Boolean returnDocs = CustomElandRerankTaskSettings.of(modelSettings, requestSettings).returnDocuments(); Function inputSupplier = returnDocs == Boolean.TRUE ? inputs::get : i -> null; - ActionListener mlResultsListener = listener.delegateFailureAndWrap( - (l, inferenceResult) -> l.onResponse(textSimilarityResultsToRankedDocs(inferenceResult.getInferenceResults(), inputSupplier)) - ); - - var maybeDeployListener = mlResultsListener.delegateResponse( - (l, exception) -> maybeStartDeployment(model, exception, request, mlResultsListener) + client.execute( + InferModelAction.INSTANCE, + request, + listener.delegateFailureAndWrap( + (l, inferenceResult) -> l.onResponse( + textSimilarityResultsToRankedDocs(inferenceResult.getInferenceResults(), inputSupplier) + ) + ) ); - - client.execute(InferModelAction.INSTANCE, request, maybeDeployListener); } public void chunkedInfer( @@ -834,8 +823,7 @@ private RankedDocsResults textSimilarityResultsToRankedDocs( public List defaultConfigIds() { return List.of( new DefaultConfigId(DEFAULT_ELSER_ID, TaskType.SPARSE_EMBEDDING, this), - new DefaultConfigId(DEFAULT_E5_ID, TaskType.TEXT_EMBEDDING, this), - new DefaultConfigId(DEFAULT_RERANK_ID, TaskType.RERANK, this) + new DefaultConfigId(DEFAULT_E5_ID, TaskType.TEXT_EMBEDDING, this) ); } @@ -928,19 +916,12 @@ private List defaultConfigs(boolean useLinuxOptimizedModel) { ), ChunkingSettingsBuilder.DEFAULT_SETTINGS ); - var defaultRerank = new ElasticRerankerModel( - DEFAULT_RERANK_ID, - TaskType.RERANK, - NAME, - new ElasticRerankerServiceSettings(null, 1, RERANKER_ID, new AdaptiveAllocationsSettings(Boolean.TRUE, 0, 32)), - RerankTaskSettings.DEFAULT_SETTINGS - ); - return List.of(defaultElser, defaultE5, defaultRerank); + return List.of(defaultElser, defaultE5); } @Override boolean isDefaultId(String inferenceId) { - return DEFAULT_ELSER_ID.equals(inferenceId) || DEFAULT_E5_ID.equals(inferenceId) || DEFAULT_RERANK_ID.equals(inferenceId); + return DEFAULT_ELSER_ID.equals(inferenceId) || DEFAULT_E5_ID.equals(inferenceId); } static EmbeddingRequestChunker.EmbeddingType embeddingTypeFromTaskTypeAndSettings( diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettingsTests.java similarity index 53% rename from x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettingsTests.java rename to x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettingsTests.java index 255454a1ed62..4207896fc54f 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettingsTests.java @@ -22,7 +22,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; -public class RerankTaskSettingsTests extends AbstractWireSerializingTestCase { +public class CustomElandRerankTaskSettingsTests extends AbstractWireSerializingTestCase { public void testIsEmpty() { var randomSettings = createRandom(); @@ -35,9 +35,9 @@ public void testUpdatedTaskSettings() { var newSettings = createRandom(); Map newSettingsMap = new HashMap<>(); if (newSettings.returnDocuments() != null) { - newSettingsMap.put(RerankTaskSettings.RETURN_DOCUMENTS, newSettings.returnDocuments()); + newSettingsMap.put(CustomElandRerankTaskSettings.RETURN_DOCUMENTS, newSettings.returnDocuments()); } - RerankTaskSettings updatedSettings = (RerankTaskSettings) initialSettings.updatedTaskSettings( + CustomElandRerankTaskSettings updatedSettings = (CustomElandRerankTaskSettings) initialSettings.updatedTaskSettings( Collections.unmodifiableMap(newSettingsMap) ); if (newSettings.returnDocuments() == null) { @@ -48,37 +48,37 @@ public void testUpdatedTaskSettings() { } public void testDefaultsFromMap_MapIsNull_ReturnsDefaultSettings() { - var rerankTaskSettings = RerankTaskSettings.defaultsFromMap(null); + var customElandRerankTaskSettings = CustomElandRerankTaskSettings.defaultsFromMap(null); - assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); } public void testDefaultsFromMap_MapIsEmpty_ReturnsDefaultSettings() { - var rerankTaskSettings = RerankTaskSettings.defaultsFromMap(new HashMap<>()); + var customElandRerankTaskSettings = CustomElandRerankTaskSettings.defaultsFromMap(new HashMap<>()); - assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); } public void testDefaultsFromMap_ExtractedReturnDocumentsNull_SetsReturnDocumentToTrue() { - var rerankTaskSettings = RerankTaskSettings.defaultsFromMap(new HashMap<>()); + var customElandRerankTaskSettings = CustomElandRerankTaskSettings.defaultsFromMap(new HashMap<>()); - assertThat(rerankTaskSettings.returnDocuments(), is(Boolean.TRUE)); + assertThat(customElandRerankTaskSettings.returnDocuments(), is(Boolean.TRUE)); } public void testFromMap_MapIsNull_ReturnsDefaultSettings() { - var rerankTaskSettings = RerankTaskSettings.fromMap(null); + var customElandRerankTaskSettings = CustomElandRerankTaskSettings.fromMap(null); - assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); } public void testFromMap_MapIsEmpty_ReturnsDefaultSettings() { - var rerankTaskSettings = RerankTaskSettings.fromMap(new HashMap<>()); + var customElandRerankTaskSettings = CustomElandRerankTaskSettings.fromMap(new HashMap<>()); - assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); } public void testToXContent_WritesAllValues() throws IOException { - var serviceSettings = new RerankTaskSettings(Boolean.TRUE); + var serviceSettings = new CustomElandRerankTaskSettings(Boolean.TRUE); XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); serviceSettings.toXContent(builder, null); @@ -89,30 +89,30 @@ public void testToXContent_WritesAllValues() throws IOException { } public void testOf_PrefersNonNullRequestTaskSettings() { - var originalSettings = new RerankTaskSettings(Boolean.FALSE); - var requestTaskSettings = new RerankTaskSettings(Boolean.TRUE); + var originalSettings = new CustomElandRerankTaskSettings(Boolean.FALSE); + var requestTaskSettings = new CustomElandRerankTaskSettings(Boolean.TRUE); - var taskSettings = RerankTaskSettings.of(originalSettings, requestTaskSettings); + var taskSettings = CustomElandRerankTaskSettings.of(originalSettings, requestTaskSettings); assertThat(taskSettings, sameInstance(requestTaskSettings)); } - private static RerankTaskSettings createRandom() { - return new RerankTaskSettings(randomOptionalBoolean()); + private static CustomElandRerankTaskSettings createRandom() { + return new CustomElandRerankTaskSettings(randomOptionalBoolean()); } @Override - protected Writeable.Reader instanceReader() { - return RerankTaskSettings::new; + protected Writeable.Reader instanceReader() { + return CustomElandRerankTaskSettings::new; } @Override - protected RerankTaskSettings createTestInstance() { + protected CustomElandRerankTaskSettings createTestInstance() { return createRandom(); } @Override - protected RerankTaskSettings mutateInstance(RerankTaskSettings instance) throws IOException { - return randomValueOtherThan(instance, RerankTaskSettingsTests::createRandom); + protected CustomElandRerankTaskSettings mutateInstance(CustomElandRerankTaskSettings instance) throws IOException { + return randomValueOtherThan(instance, CustomElandRerankTaskSettingsTests::createRandom); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java index 17e6583f11c8..306509ea60cf 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java @@ -534,13 +534,16 @@ public void testParseRequestConfig_Rerank() { ) ); var returnDocs = randomBoolean(); - settings.put(ModelConfigurations.TASK_SETTINGS, new HashMap<>(Map.of(RerankTaskSettings.RETURN_DOCUMENTS, returnDocs))); + settings.put( + ModelConfigurations.TASK_SETTINGS, + new HashMap<>(Map.of(CustomElandRerankTaskSettings.RETURN_DOCUMENTS, returnDocs)) + ); ActionListener modelListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(CustomElandRerankModel.class)); - assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); + assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); assertThat(model.getServiceSettings(), instanceOf(CustomElandInternalServiceSettings.class)); - assertEquals(returnDocs, ((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertEquals(returnDocs, ((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); }, e -> { fail("Model parsing failed " + e.getMessage()); }); service.parseRequestConfig(randomInferenceEntityId, TaskType.RERANK, settings, modelListener); @@ -580,9 +583,9 @@ public void testParseRequestConfig_Rerank_DefaultTaskSettings() { ActionListener modelListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(CustomElandRerankModel.class)); - assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); + assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); assertThat(model.getServiceSettings(), instanceOf(CustomElandInternalServiceSettings.class)); - assertEquals(Boolean.TRUE, ((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertEquals(Boolean.TRUE, ((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); }, e -> { fail("Model parsing failed " + e.getMessage()); }); service.parseRequestConfig(randomInferenceEntityId, TaskType.RERANK, settings, modelListener); @@ -1246,11 +1249,14 @@ public void testParsePersistedConfig_Rerank() { ); settings.put(ElasticsearchInternalServiceSettings.MODEL_ID, "foo"); var returnDocs = randomBoolean(); - settings.put(ModelConfigurations.TASK_SETTINGS, new HashMap<>(Map.of(RerankTaskSettings.RETURN_DOCUMENTS, returnDocs))); + settings.put( + ModelConfigurations.TASK_SETTINGS, + new HashMap<>(Map.of(CustomElandRerankTaskSettings.RETURN_DOCUMENTS, returnDocs)) + ); var model = service.parsePersistedConfig(randomInferenceEntityId, TaskType.RERANK, settings); - assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); - assertEquals(returnDocs, ((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); + assertEquals(returnDocs, ((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); } // without task settings @@ -1273,8 +1279,8 @@ public void testParsePersistedConfig_Rerank() { settings.put(ElasticsearchInternalServiceSettings.MODEL_ID, "foo"); var model = service.parsePersistedConfig(randomInferenceEntityId, TaskType.RERANK, settings); - assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); - assertTrue(((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); + assertTrue(((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); } } @@ -1329,7 +1335,7 @@ private CustomElandModel getCustomElandModel(TaskType taskType) { taskType, ElasticsearchInternalService.NAME, new CustomElandInternalServiceSettings(1, 4, "custom-model", null), - RerankTaskSettings.DEFAULT_SETTINGS + CustomElandRerankTaskSettings.DEFAULT_SETTINGS ); } else if (taskType == TaskType.TEXT_EMBEDDING) { var serviceSettings = new CustomElandInternalTextEmbeddingServiceSettings(1, 4, "custom-model", null); @@ -1522,30 +1528,20 @@ public void testEmbeddingTypeFromTaskTypeAndSettings() { ) ); - var e1 = expectThrows( + var e = expectThrows( ElasticsearchStatusException.class, () -> ElasticsearchInternalService.embeddingTypeFromTaskTypeAndSettings( TaskType.COMPLETION, new ElasticsearchInternalServiceSettings(1, 1, "foo", null) ) ); - assertThat(e1.getMessage(), containsString("Chunking is not supported for task type [completion]")); - - var e2 = expectThrows( - ElasticsearchStatusException.class, - () -> ElasticsearchInternalService.embeddingTypeFromTaskTypeAndSettings( - TaskType.RERANK, - new ElasticsearchInternalServiceSettings(1, 1, "foo", null) - ) - ); - assertThat(e2.getMessage(), containsString("Chunking is not supported for task type [rerank]")); + assertThat(e.getMessage(), containsString("Chunking is not supported for task type [completion]")); } public void testIsDefaultId() { var service = createService(mock(Client.class)); assertTrue(service.isDefaultId(".elser-2-elasticsearch")); assertTrue(service.isDefaultId(".multilingual-e5-small-elasticsearch")); - assertTrue(service.isDefaultId(".rerank-v1-elasticsearch")); assertFalse(service.isDefaultId("foo")); } From ec66857ca13e2f5e7f9088a30aa48ea5ddab17fa Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Mon, 9 Dec 2024 10:03:14 +0000 Subject: [PATCH 06/39] Remove pre-7.2 token serialization support (#118057) --- .../org/elasticsearch/TransportVersions.java | 2 - .../security/SecurityFeatureSetUsage.java | 12 +- .../support/TokensInvalidationResult.java | 6 - .../security/authc/TokenAuthIntegTests.java | 37 ++- .../xpack/security/authc/TokenService.java | 236 +++++------------ .../security/authc/TokenServiceTests.java | 241 +----------------- 6 files changed, 88 insertions(+), 446 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 1a1219825bbb..40a209c5f0f1 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -54,11 +54,9 @@ static TransportVersion def(int id) { public static final TransportVersion ZERO = def(0); public static final TransportVersion V_7_0_0 = def(7_00_00_99); public static final TransportVersion V_7_0_1 = def(7_00_01_99); - public static final TransportVersion V_7_1_0 = def(7_01_00_99); public static final TransportVersion V_7_2_0 = def(7_02_00_99); public static final TransportVersion V_7_2_1 = def(7_02_01_99); public static final TransportVersion V_7_3_0 = def(7_03_00_99); - public static final TransportVersion V_7_3_2 = def(7_03_02_99); public static final TransportVersion V_7_4_0 = def(7_04_00_99); public static final TransportVersion V_7_5_0 = def(7_05_00_99); public static final TransportVersion V_7_6_0 = def(7_06_00_99); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java index 2793ddea3bd0..33f1a9a469b6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java @@ -55,10 +55,8 @@ public SecurityFeatureSetUsage(StreamInput in) throws IOException { realmsUsage = in.readGenericMap(); rolesStoreUsage = in.readGenericMap(); sslUsage = in.readGenericMap(); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) { - tokenServiceUsage = in.readGenericMap(); - apiKeyServiceUsage = in.readGenericMap(); - } + tokenServiceUsage = in.readGenericMap(); + apiKeyServiceUsage = in.readGenericMap(); auditUsage = in.readGenericMap(); ipFilterUsage = in.readGenericMap(); anonymousUsage = in.readGenericMap(); @@ -125,10 +123,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeGenericMap(realmsUsage); out.writeGenericMap(rolesStoreUsage); out.writeGenericMap(sslUsage); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) { - out.writeGenericMap(tokenServiceUsage); - out.writeGenericMap(apiKeyServiceUsage); - } + out.writeGenericMap(tokenServiceUsage); + out.writeGenericMap(apiKeyServiceUsage); out.writeGenericMap(auditUsage); out.writeGenericMap(ipFilterUsage); out.writeGenericMap(anonymousUsage); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java index 8fe018a82546..59c16fc8a7a7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/TokensInvalidationResult.java @@ -59,9 +59,6 @@ public TokensInvalidationResult(StreamInput in) throws IOException { this.invalidatedTokens = in.readStringCollectionAsList(); this.previouslyInvalidatedTokens = in.readStringCollectionAsList(); this.errors = in.readCollectionAsList(StreamInput::readException); - if (in.getTransportVersion().before(TransportVersions.V_7_2_0)) { - in.readVInt(); - } if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_0_0)) { this.restStatus = RestStatus.readFrom(in); } @@ -111,9 +108,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeStringCollection(invalidatedTokens); out.writeStringCollection(previouslyInvalidatedTokens); out.writeCollection(errors, StreamOutput::writeException); - if (out.getTransportVersion().before(TransportVersions.V_7_2_0)) { - out.writeVInt(5); - } if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_0_0)) { RestStatus.writeTo(out, restStatus); } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java index fef1a98ca67e..b56ea7ae3e45 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java @@ -327,8 +327,8 @@ public void testInvalidateNotValidAccessTokens() throws Exception { ResponseException.class, () -> invalidateAccessToken( tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.V_7_3_2, - tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, randomBoolean()).v1() + TransportVersions.MINIMUM_COMPATIBLE, + tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, randomBoolean()).v1() ) ) ); @@ -347,7 +347,7 @@ public void testInvalidateNotValidAccessTokens() throws Exception { byte[] longerAccessToken = new byte[randomIntBetween(17, 24)]; random().nextBytes(longerAccessToken); invalidateResponse = invalidateAccessToken( - tokenService.prependVersionAndEncodeAccessToken(TransportVersions.V_7_3_2, longerAccessToken) + tokenService.prependVersionAndEncodeAccessToken(TransportVersions.MINIMUM_COMPATIBLE, longerAccessToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -365,7 +365,7 @@ public void testInvalidateNotValidAccessTokens() throws Exception { byte[] shorterAccessToken = new byte[randomIntBetween(12, 15)]; random().nextBytes(shorterAccessToken); invalidateResponse = invalidateAccessToken( - tokenService.prependVersionAndEncodeAccessToken(TransportVersions.V_7_3_2, shorterAccessToken) + tokenService.prependVersionAndEncodeAccessToken(TransportVersions.MINIMUM_COMPATIBLE, shorterAccessToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -394,8 +394,8 @@ public void testInvalidateNotValidAccessTokens() throws Exception { invalidateResponse = invalidateAccessToken( tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.V_7_3_2, - tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, randomBoolean()).v1() + TransportVersions.MINIMUM_COMPATIBLE, + tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, randomBoolean()).v1() ) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); @@ -420,8 +420,8 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { ResponseException.class, () -> invalidateRefreshToken( TokenService.prependVersionAndEncodeRefreshToken( - TransportVersions.V_7_3_2, - tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, true).v2() + TransportVersions.MINIMUM_COMPATIBLE, + tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, true).v2() ) ) ); @@ -441,7 +441,7 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { byte[] longerRefreshToken = new byte[randomIntBetween(17, 24)]; random().nextBytes(longerRefreshToken); invalidateResponse = invalidateRefreshToken( - TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.V_7_3_2, longerRefreshToken) + TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.MINIMUM_COMPATIBLE, longerRefreshToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -459,7 +459,7 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { byte[] shorterRefreshToken = new byte[randomIntBetween(12, 15)]; random().nextBytes(shorterRefreshToken); invalidateResponse = invalidateRefreshToken( - TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.V_7_3_2, shorterRefreshToken) + TokenService.prependVersionAndEncodeRefreshToken(TransportVersions.MINIMUM_COMPATIBLE, shorterRefreshToken) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); assertThat(invalidateResponse.previouslyInvalidated(), equalTo(0)); @@ -488,8 +488,8 @@ public void testInvalidateNotValidRefreshTokens() throws Exception { invalidateResponse = invalidateRefreshToken( TokenService.prependVersionAndEncodeRefreshToken( - TransportVersions.V_7_3_2, - tokenService.getRandomTokenBytes(TransportVersions.V_7_3_2, true).v2() + TransportVersions.MINIMUM_COMPATIBLE, + tokenService.getRandomTokenBytes(TransportVersions.MINIMUM_COMPATIBLE, true).v2() ) ); assertThat(invalidateResponse.invalidated(), equalTo(0)); @@ -758,18 +758,11 @@ public void testAuthenticateWithWrongToken() throws Exception { assertAuthenticateWithToken(response.accessToken(), TEST_USER_NAME); // Now attempt to authenticate with an invalid access token string assertUnauthorizedToken(randomAlphaOfLengthBetween(0, 128)); - // Now attempt to authenticate with an invalid access token with valid structure (pre 7.2) + // Now attempt to authenticate with an invalid access token with valid structure (after 8.0 pre 8.10) assertUnauthorizedToken( tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.V_7_1_0, - tokenService.getRandomTokenBytes(TransportVersions.V_7_1_0, randomBoolean()).v1() - ) - ); - // Now attempt to authenticate with an invalid access token with valid structure (after 7.2 pre 8.10) - assertUnauthorizedToken( - tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.V_7_4_0, - tokenService.getRandomTokenBytes(TransportVersions.V_7_4_0, randomBoolean()).v1() + TransportVersions.V_8_0_0, + tokenService.getRandomTokenBytes(TransportVersions.V_8_0_0, randomBoolean()).v1() ) ); // Now attempt to authenticate with an invalid access token with valid structure (current version) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 4f7ba7808b82..900436a1fd87 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -48,9 +48,7 @@ import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.InputStreamStreamInput; -import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -59,7 +57,6 @@ import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; -import org.elasticsearch.core.Streams; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; @@ -93,10 +90,8 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; -import java.io.OutputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -132,7 +127,6 @@ import javax.crypto.Cipher; import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; @@ -201,14 +195,8 @@ public class TokenService { // UUIDs are 16 bytes encoded base64 without padding, therefore the length is (16 / 3) * 4 + ((16 % 3) * 8 + 5) / 6 chars private static final int TOKEN_LENGTH = 22; private static final String TOKEN_DOC_ID_PREFIX = TOKEN_DOC_TYPE + "_"; - static final int LEGACY_MINIMUM_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES + 1; static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_LENGTH + 1; - static final int LEGACY_MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * LEGACY_MINIMUM_BYTES) / 3)).intValue(); public static final int MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * MINIMUM_BYTES) / 3)).intValue(); - static final TransportVersion VERSION_HASHED_TOKENS = TransportVersions.V_7_2_0; - static final TransportVersion VERSION_TOKENS_INDEX_INTRODUCED = TransportVersions.V_7_2_0; - static final TransportVersion VERSION_ACCESS_TOKENS_AS_UUIDS = TransportVersions.V_7_2_0; - static final TransportVersion VERSION_MULTIPLE_CONCURRENT_REFRESHES = TransportVersions.V_7_2_0; static final TransportVersion VERSION_CLIENT_AUTH_FOR_REFRESH = TransportVersions.V_8_2_0; static final TransportVersion VERSION_GET_TOKEN_DOC_FOR_REFRESH = TransportVersions.V_8_10_X; @@ -273,8 +261,7 @@ public TokenService( /** * Creates an access token and optionally a refresh token as well, based on the provided authentication and metadata with - * auto-generated values. The created tokens are stored in the security index for versions up to - * {@link #VERSION_TOKENS_INDEX_INTRODUCED} and to a specific security tokens index for later versions. + * auto-generated values. The created tokens are stored a specific security tokens index. */ public void createOAuth2Tokens( Authentication authentication, @@ -291,8 +278,7 @@ public void createOAuth2Tokens( /** * Creates an access token and optionally a refresh token as well from predefined values, based on the provided authentication and - * metadata. The created tokens are stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED} and to a - * specific security tokens index for later versions. + * metadata. The created tokens are stored in a specific security tokens index. */ // public for testing public void createOAuth2Tokens( @@ -314,21 +300,15 @@ public void createOAuth2Tokens( * * @param accessTokenBytes The predefined seed value for the access token. This will then be *
    - *
  • Encrypted before stored for versions before {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in a specific security tokens index for versions after - * {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Hashed before stored
  • + *
  • Stored in a specific security tokens index
  • *
  • Prepended with a version ID and Base64 encoded before returned to the caller of the APIs
  • *
* @param refreshTokenBytes The predefined seed value for the access token. This will then be *
    - *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in a specific security tokens index for versions after - * {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs - * for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Hashed before stored
  • + *
  • Stored in a specific security tokens index
  • + *
  • Prepended with a version ID and Base64 encoded before returned to the caller of the APIs
  • *
* @param tokenVersion The version of the nodes with which these tokens will be compatible. * @param authentication The authentication object representing the user for which the tokens are created @@ -384,7 +364,7 @@ private void createOAuth2Tokens( } else { refreshTokenToStore = refreshTokenToReturn = null; } - } else if (tokenVersion.onOrAfter(VERSION_HASHED_TOKENS)) { + } else { assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH; userTokenId = hashTokenString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes)); accessTokenToStore = null; @@ -395,18 +375,6 @@ private void createOAuth2Tokens( } else { refreshTokenToStore = refreshTokenToReturn = null; } - } else { - assert accessTokenBytes.length == RAW_TOKEN_BYTES_LENGTH; - userTokenId = Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes); - accessTokenToStore = null; - if (refreshTokenBytes != null) { - assert refreshTokenBytes.length == RAW_TOKEN_BYTES_LENGTH; - refreshTokenToStore = refreshTokenToReturn = Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString( - refreshTokenBytes - ); - } else { - refreshTokenToStore = refreshTokenToReturn = null; - } } UserToken userToken = new UserToken(userTokenId, tokenVersion, tokenAuth, getExpirationTime(), metadata); tokenDocument = createTokenDocument(userToken, accessTokenToStore, refreshTokenToStore, originatingClientAuth); @@ -419,23 +387,22 @@ private void createOAuth2Tokens( final RefreshPolicy tokenCreationRefreshPolicy = tokenVersion.onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH) ? RefreshPolicy.NONE : RefreshPolicy.WAIT_UNTIL; - final SecurityIndexManager tokensIndex = getTokensIndexForVersion(tokenVersion); logger.debug( () -> format( "Using refresh policy [%s] when creating token doc [%s] in the security index [%s]", tokenCreationRefreshPolicy, documentId, - tokensIndex.aliasName() + securityTokensIndex.aliasName() ) ); - final IndexRequest indexTokenRequest = client.prepareIndex(tokensIndex.aliasName()) + final IndexRequest indexTokenRequest = client.prepareIndex(securityTokensIndex.aliasName()) .setId(documentId) .setOpType(OpType.CREATE) .setSource(tokenDocument, XContentType.JSON) .setRefreshPolicy(tokenCreationRefreshPolicy) .request(); - tokensIndex.prepareIndexIfNeededThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)), + securityTokensIndex.prepareIndexIfNeededThenExecute( + ex -> listener.onFailure(traceLog("prepare tokens index [" + securityTokensIndex.aliasName() + "]", documentId, ex)), () -> executeAsyncWithOrigin( client, SECURITY_ORIGIN, @@ -554,17 +521,16 @@ private void getTokenDocById( @Nullable String storedRefreshToken, ActionListener listener ) { - final SecurityIndexManager tokensIndex = getTokensIndexForVersion(tokenVersion); - final SecurityIndexManager frozenTokensIndex = tokensIndex.defensiveCopy(); + final SecurityIndexManager frozenTokensIndex = securityTokensIndex.defensiveCopy(); if (frozenTokensIndex.isAvailable(PRIMARY_SHARDS) == false) { - logger.warn("failed to get access token [{}] because index [{}] is not available", tokenId, tokensIndex.aliasName()); + logger.warn("failed to get access token [{}] because index [{}] is not available", tokenId, securityTokensIndex.aliasName()); listener.onFailure(frozenTokensIndex.getUnavailableReason(PRIMARY_SHARDS)); return; } - final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), getTokenDocumentId(tokenId)).request(); + final GetRequest getRequest = client.prepareGet(securityTokensIndex.aliasName(), getTokenDocumentId(tokenId)).request(); final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", tokenId, ex)); - tokensIndex.checkIndexVersionThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenId, ex)), + securityTokensIndex.checkIndexVersionThenExecute( + ex -> listener.onFailure(traceLog("prepare tokens index [" + securityTokensIndex.aliasName() + "]", tokenId, ex)), () -> executeAsyncWithOrigin( client.threadPool().getThreadContext(), SECURITY_ORIGIN, @@ -610,7 +576,11 @@ private void getTokenDocById( // if the index or the shard is not there / available we assume that // the token is not valid if (isShardNotAvailableException(e)) { - logger.warn("failed to get token doc [{}] because index [{}] is not available", tokenId, tokensIndex.aliasName()); + logger.warn( + "failed to get token doc [{}] because index [{}] is not available", + tokenId, + securityTokensIndex.aliasName() + ); } else { logger.error(() -> "failed to get token doc [" + tokenId + "]", e); } @@ -650,7 +620,7 @@ void decodeToken(String token, boolean validateUserToken, ActionListener VERSION_ACCESS_TOKENS_UUIDS cluster if (in.available() < MINIMUM_BYTES) { logger.debug("invalid token, smaller than [{}] bytes", MINIMUM_BYTES); @@ -660,41 +630,6 @@ void decodeToken(String token, boolean validateUserToken, ActionListener { - if (decodeKey != null) { - try { - final Cipher cipher = getDecryptionCipher(iv, decodeKey, version, decodedSalt); - final String tokenId = decryptTokenId(encryptedTokenId, cipher, version); - getAndValidateUserToken(tokenId, version, null, validateUserToken, listener); - } catch (IOException | GeneralSecurityException e) { - // could happen with a token that is not ours - logger.warn("invalid token", e); - listener.onResponse(null); - } - } else { - // could happen with a token that is not ours - listener.onResponse(null); - } - }, listener::onFailure)); - } else { - logger.debug(() -> format("invalid key %s key: %s", passphraseHash, keyCache.cache.keySet())); - listener.onResponse(null); - } } } catch (Exception e) { // could happen with a token that is not ours @@ -852,11 +787,7 @@ private void indexInvalidation( final Set idsOfOlderTokens = new HashSet<>(); boolean anyOlderTokensBeforeRefreshViaGet = false; for (UserToken userToken : userTokens) { - if (userToken.getTransportVersion().onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { - idsOfRecentTokens.add(userToken.getId()); - } else { - idsOfOlderTokens.add(userToken.getId()); - } + idsOfRecentTokens.add(userToken.getId()); anyOlderTokensBeforeRefreshViaGet |= userToken.getTransportVersion().before(VERSION_GET_TOKEN_DOC_FOR_REFRESH); } final RefreshPolicy tokensInvalidationRefreshPolicy = anyOlderTokensBeforeRefreshViaGet @@ -1124,7 +1055,7 @@ private void findTokenFromRefreshToken(String refreshToken, Iterator ); getTokenDocById(userTokenId, version, null, storedRefreshToken, listener); } - } else if (version.onOrAfter(VERSION_HASHED_TOKENS)) { + } else { final String unencodedRefreshToken = in.readString(); if (unencodedRefreshToken.length() != TOKEN_LENGTH) { logger.debug("Decoded refresh token [{}] with version [{}] is invalid.", unencodedRefreshToken, version); @@ -1133,9 +1064,6 @@ private void findTokenFromRefreshToken(String refreshToken, Iterator final String hashedRefreshToken = hashTokenString(unencodedRefreshToken); findTokenFromRefreshToken(hashedRefreshToken, securityTokensIndex, backoff, listener); } - } else { - logger.debug("Unrecognized refresh token version [{}].", version); - listener.onResponse(null); } } catch (IOException e) { logger.debug(() -> "Could not decode refresh token [" + refreshToken + "].", e); @@ -1250,7 +1178,6 @@ private void innerRefresh( return; } final RefreshTokenStatus refreshTokenStatus = checkRefreshResult.v1(); - final SecurityIndexManager refreshedTokenIndex = getTokensIndexForVersion(refreshTokenStatus.getTransportVersion()); if (refreshTokenStatus.isRefreshed()) { logger.debug( "Token document [{}] was recently refreshed, when a new token document was generated. Reusing that result.", @@ -1258,31 +1185,29 @@ private void innerRefresh( ); final Tuple parsedTokens = parseTokensFromDocument(tokenDoc.sourceAsMap(), null); Authentication authentication = parsedTokens.v1().getAuthentication(); - decryptAndReturnSupersedingTokens(refreshToken, refreshTokenStatus, refreshedTokenIndex, authentication, listener); + decryptAndReturnSupersedingTokens(refreshToken, refreshTokenStatus, securityTokensIndex, authentication, listener); } else { final TransportVersion newTokenVersion = getTokenVersionCompatibility(); final Tuple newTokenBytes = getRandomTokenBytes(newTokenVersion, true); final Map updateMap = new HashMap<>(); updateMap.put("refreshed", true); - if (newTokenVersion.onOrAfter(VERSION_MULTIPLE_CONCURRENT_REFRESHES)) { - updateMap.put("refresh_time", clock.instant().toEpochMilli()); - try { - final byte[] iv = getRandomBytes(IV_BYTES); - final byte[] salt = getRandomBytes(SALT_BYTES); - String encryptedAccessAndRefreshToken = encryptSupersedingTokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - refreshToken, - iv, - salt - ); - updateMap.put("superseding.encrypted_tokens", encryptedAccessAndRefreshToken); - updateMap.put("superseding.encryption_iv", Base64.getEncoder().encodeToString(iv)); - updateMap.put("superseding.encryption_salt", Base64.getEncoder().encodeToString(salt)); - } catch (GeneralSecurityException e) { - logger.warn("could not encrypt access token and refresh token string", e); - onFailure.accept(invalidGrantException("could not refresh the requested token")); - } + updateMap.put("refresh_time", clock.instant().toEpochMilli()); + try { + final byte[] iv = getRandomBytes(IV_BYTES); + final byte[] salt = getRandomBytes(SALT_BYTES); + String encryptedAccessAndRefreshToken = encryptSupersedingTokens( + newTokenBytes.v1(), + newTokenBytes.v2(), + refreshToken, + iv, + salt + ); + updateMap.put("superseding.encrypted_tokens", encryptedAccessAndRefreshToken); + updateMap.put("superseding.encryption_iv", Base64.getEncoder().encodeToString(iv)); + updateMap.put("superseding.encryption_salt", Base64.getEncoder().encodeToString(salt)); + } catch (GeneralSecurityException e) { + logger.warn("could not encrypt access token and refresh token string", e); + onFailure.accept(invalidGrantException("could not refresh the requested token")); } assert tokenDoc.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO : "expected an assigned sequence number"; assert tokenDoc.primaryTerm() != SequenceNumbers.UNASSIGNED_PRIMARY_TERM : "expected an assigned primary term"; @@ -1293,17 +1218,17 @@ private void innerRefresh( "Using refresh policy [%s] when updating token doc [%s] for refresh in the security index [%s]", tokenRefreshUpdateRefreshPolicy, tokenDoc.id(), - refreshedTokenIndex.aliasName() + securityTokensIndex.aliasName() ) ); - final UpdateRequestBuilder updateRequest = client.prepareUpdate(refreshedTokenIndex.aliasName(), tokenDoc.id()) + final UpdateRequestBuilder updateRequest = client.prepareUpdate(securityTokensIndex.aliasName(), tokenDoc.id()) .setDoc("refresh_token", updateMap) .setFetchSource(logger.isDebugEnabled()) .setRefreshPolicy(tokenRefreshUpdateRefreshPolicy) .setIfSeqNo(tokenDoc.seqNo()) .setIfPrimaryTerm(tokenDoc.primaryTerm()); - refreshedTokenIndex.prepareIndexIfNeededThenExecute( - ex -> listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)), + securityTokensIndex.prepareIndexIfNeededThenExecute( + ex -> listener.onFailure(traceLog("prepare index [" + securityTokensIndex.aliasName() + "]", ex)), () -> executeAsyncWithOrigin( client.threadPool().getThreadContext(), SECURITY_ORIGIN, @@ -1349,7 +1274,7 @@ private void innerRefresh( if (cause instanceof VersionConflictEngineException) { // The document has been updated by another thread, get it again. logger.debug("version conflict while updating document [{}], attempting to get it again", tokenDoc.id()); - getTokenDocAsync(tokenDoc.id(), refreshedTokenIndex, true, new ActionListener<>() { + getTokenDocAsync(tokenDoc.id(), securityTokensIndex, true, new ActionListener<>() { @Override public void onResponse(GetResponse response) { if (response.isExists()) { @@ -1368,7 +1293,7 @@ public void onFailure(Exception e) { logger.info("could not get token document [{}] for refresh, retrying", tokenDoc.id()); client.threadPool() .schedule( - () -> getTokenDocAsync(tokenDoc.id(), refreshedTokenIndex, true, this), + () -> getTokenDocAsync(tokenDoc.id(), securityTokensIndex, true, this), backoff.next(), client.threadPool().generic() ); @@ -1689,17 +1614,13 @@ private static Optional checkMultipleRefreshes( RefreshTokenStatus refreshTokenStatus ) { if (refreshTokenStatus.isRefreshed()) { - if (refreshTokenStatus.getTransportVersion().onOrAfter(VERSION_MULTIPLE_CONCURRENT_REFRESHES)) { - if (refreshRequested.isAfter(refreshTokenStatus.getRefreshInstant().plus(30L, ChronoUnit.SECONDS))) { - return Optional.of(invalidGrantException("token has already been refreshed more than 30 seconds in the past")); - } - if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) { - return Optional.of( - invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great") - ); - } - } else { - return Optional.of(invalidGrantException("token has already been refreshed")); + if (refreshRequested.isAfter(refreshTokenStatus.getRefreshInstant().plus(30L, ChronoUnit.SECONDS))) { + return Optional.of(invalidGrantException("token has already been refreshed more than 30 seconds in the past")); + } + if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) { + return Optional.of( + invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great") + ); } } return Optional.empty(); @@ -1979,21 +1900,6 @@ private void ensureEnabled() { } } - /** - * In version {@code #VERSION_TOKENS_INDEX_INTRODUCED} security tokens were moved into a separate index, away from the other entities in - * the main security index, due to their ephemeral nature. They moved "seamlessly" - without manual user intervention. In this way, new - * tokens are created in the new index, while the existing ones were left in place - to be accessed from the old index - and due to be - * removed automatically by the {@code ExpiredTokenRemover} periodic job. Therefore, in general, when searching for a token we need to - * consider both the new and the old indices. - */ - private SecurityIndexManager getTokensIndexForVersion(TransportVersion version) { - if (version.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { - return securityTokensIndex; - } else { - return securityMainIndex; - } - } - public TimeValue getExpirationDelay() { return expirationDelay; } @@ -2022,41 +1928,13 @@ public String prependVersionAndEncodeAccessToken(TransportVersion version, byte[ out.writeByteArray(accessTokenBytes); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } - } else if (version.onOrAfter(VERSION_ACCESS_TOKENS_AS_UUIDS)) { + } else { try (BytesStreamOutput out = new BytesStreamOutput(MINIMUM_BASE64_BYTES)) { out.setTransportVersion(version); TransportVersion.writeVersion(version, out); out.writeString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes)); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } - } else { - // we know that the minimum length is larger than the default of the ByteArrayOutputStream so set the size to this explicitly - try ( - ByteArrayOutputStream os = new ByteArrayOutputStream(LEGACY_MINIMUM_BASE64_BYTES); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64) - ) { - out.setTransportVersion(version); - KeyAndCache keyAndCache = keyCache.activeKeyCache; - TransportVersion.writeVersion(version, out); - out.writeByteArray(keyAndCache.getSalt().bytes); - out.writeByteArray(keyAndCache.getKeyHash().bytes); - final byte[] initializationVector = getRandomBytes(IV_BYTES); - out.writeByteArray(initializationVector); - try ( - CipherOutputStream encryptedOutput = new CipherOutputStream( - out, - getEncryptionCipher(initializationVector, keyAndCache, version) - ); - StreamOutput encryptedStreamOutput = new OutputStreamStreamOutput(encryptedOutput) - ) { - encryptedStreamOutput.setTransportVersion(version); - encryptedStreamOutput.writeString(Strings.BASE_64_NO_PADDING_URL_ENCODER.encodeToString(accessTokenBytes)); - // StreamOutput needs to be closed explicitly because it wraps CipherOutputStream - encryptedStreamOutput.close(); - return new String(os.toByteArray(), StandardCharsets.UTF_8); - } - } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 75c2507a1dc5..702af7514109 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -126,7 +126,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -148,7 +147,6 @@ public class TokenServiceTests extends ESTestCase { private SecurityIndexManager securityMainIndex; private SecurityIndexManager securityTokensIndex; private ClusterService clusterService; - private DiscoveryNode pre72OldNode; private DiscoveryNode pre8500040OldNode; private Settings tokenServiceEnabledSettings = Settings.builder() .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true) @@ -228,31 +226,12 @@ public void setupClient() { licenseState = mock(MockLicenseState.class); when(licenseState.isAllowed(Security.TOKEN_SERVICE_FEATURE)).thenReturn(true); - if (randomBoolean()) { - // version 7.2 was an "inflection" point in the Token Service development (access_tokens as UUIDS, multiple concurrent - // refreshes, - // tokens docs on a separate index) - pre72OldNode = addAnother7071DataNode(this.clusterService); - } if (randomBoolean()) { // before refresh tokens used GET, i.e. TokenService#VERSION_GET_TOKEN_DOC_FOR_REFRESH pre8500040OldNode = addAnotherPre8500DataNode(this.clusterService); } } - private static DiscoveryNode addAnother7071DataNode(ClusterService clusterService) { - Version version; - TransportVersion transportVersion; - if (randomBoolean()) { - version = Version.V_7_0_0; - transportVersion = TransportVersions.V_7_0_0; - } else { - version = Version.V_7_1_0; - transportVersion = TransportVersions.V_7_1_0; - } - return addAnotherDataNodeWithVersion(clusterService, version, transportVersion); - } - private static DiscoveryNode addAnotherPre8500DataNode(ClusterService clusterService) { Version version; TransportVersion transportVersion; @@ -301,53 +280,6 @@ public static void shutdownThreadpool() { threadPool = null; } - public void testAttachAndGetToken() throws Exception { - TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - // This test only makes sense in mixed clusters with pre v7.2.0 nodes where the Token Service Key is used (to encrypt tokens) - if (null == pre72OldNode) { - pre72OldNode = addAnother7071DataNode(this.clusterService); - } - Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("joe", "admin")) - .realmRef(new RealmRef("native_realm", "native", "node1")) - .build(false); - PlainActionFuture tokenFuture = new PlainActionFuture<>(); - Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); - tokenService.createOAuth2Tokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - authentication, - authentication, - Collections.emptyMap(), - tokenFuture - ); - final String accessToken = tokenFuture.get().getAccessToken(); - assertNotNull(accessToken); - mockGetTokenFromAccessTokenBytes(tokenService, newTokenBytes.v1(), authentication, false, null); - - ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", randomFrom("Bearer ", "BEARER ", "bearer ") + accessToken); - - try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { - PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); - tokenService.tryAuthenticateToken(bearerToken, future); - UserToken serialized = future.get(); - assertAuthentication(authentication, serialized.getAuthentication()); - } - - try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { - // verify a second separate token service with its own salt can also verify - TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - anotherService.refreshMetadata(tokenService.getTokenMetadata()); - PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); - anotherService.tryAuthenticateToken(bearerToken, future); - UserToken fromOtherService = future.get(); - assertAuthentication(authentication, fromOtherService.getAuthentication()); - } - } - public void testInvalidAuthorizationHeader() throws Exception { TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); @@ -364,89 +296,6 @@ public void testInvalidAuthorizationHeader() throws Exception { } } - public void testPassphraseWorks() throws Exception { - TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - // This test only makes sense in mixed clusters with pre v7.1.0 nodes where the Key is actually used - if (null == pre72OldNode) { - pre72OldNode = addAnother7071DataNode(this.clusterService); - } - Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("joe", "admin")) - .realmRef(new RealmRef("native_realm", "native", "node1")) - .build(false); - PlainActionFuture tokenFuture = new PlainActionFuture<>(); - Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); - tokenService.createOAuth2Tokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - authentication, - authentication, - Collections.emptyMap(), - tokenFuture - ); - final String accessToken = tokenFuture.get().getAccessToken(); - assertNotNull(accessToken); - mockGetTokenFromAccessTokenBytes(tokenService, newTokenBytes.v1(), authentication, false, null); - - ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - storeTokenHeader(requestContext, accessToken); - - try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { - PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); - tokenService.tryAuthenticateToken(bearerToken, future); - UserToken serialized = future.get(); - assertAuthentication(authentication, serialized.getAuthentication()); - } - - try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { - // verify a second separate token service with its own passphrase cannot verify - TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); - anotherService.tryAuthenticateToken(bearerToken, future); - assertNull(future.get()); - } - } - - public void testGetTokenWhenKeyCacheHasExpired() throws Exception { - TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - // This test only makes sense in mixed clusters with pre v7.1.0 nodes where the Key is actually used - if (null == pre72OldNode) { - pre72OldNode = addAnother7071DataNode(this.clusterService); - } - Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("joe", "admin")) - .realmRef(new RealmRef("native_realm", "native", "node1")) - .build(false); - - PlainActionFuture tokenFuture = new PlainActionFuture<>(); - Tuple newTokenBytes = tokenService.getRandomTokenBytes(randomBoolean()); - tokenService.createOAuth2Tokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - authentication, - authentication, - Collections.emptyMap(), - tokenFuture - ); - String accessToken = tokenFuture.get().getAccessToken(); - assertThat(accessToken, notNullValue()); - - tokenService.clearActiveKeyCache(); - - tokenService.createOAuth2Tokens( - newTokenBytes.v1(), - newTokenBytes.v2(), - authentication, - authentication, - Collections.emptyMap(), - tokenFuture - ); - accessToken = tokenFuture.get().getAccessToken(); - assertThat(accessToken, notNullValue()); - } - public void testAuthnWithInvalidatedToken() throws Exception { when(securityMainIndex.indexExists()).thenReturn(true); TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); @@ -820,57 +669,6 @@ public void testMalformedRefreshTokens() throws Exception { } } - public void testNonExistingPre72Token() throws Exception { - TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - // mock another random token so that we don't find a token in TokenService#getUserTokenFromId - Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("joe", "admin")) - .realmRef(new RealmRef("native_realm", "native", "node1")) - .build(false); - mockGetTokenFromAccessTokenBytes(tokenService, tokenService.getRandomTokenBytes(randomBoolean()).v1(), authentication, false, null); - ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - storeTokenHeader( - requestContext, - tokenService.prependVersionAndEncodeAccessToken( - TransportVersions.V_7_1_0, - tokenService.getRandomTokenBytes(TransportVersions.V_7_1_0, randomBoolean()).v1() - ) - ); - - try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { - PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); - tokenService.tryAuthenticateToken(bearerToken, future); - assertNull(future.get()); - } - } - - public void testNonExistingUUIDToken() throws Exception { - TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); - // mock another random token so that we don't find a token in TokenService#getUserTokenFromId - Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("joe", "admin")) - .realmRef(new RealmRef("native_realm", "native", "node1")) - .build(false); - mockGetTokenFromAccessTokenBytes(tokenService, tokenService.getRandomTokenBytes(randomBoolean()).v1(), authentication, false, null); - ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - TransportVersion uuidTokenVersion = randomFrom(TransportVersions.V_7_2_0, TransportVersions.V_7_3_2); - storeTokenHeader( - requestContext, - tokenService.prependVersionAndEncodeAccessToken( - uuidTokenVersion, - tokenService.getRandomTokenBytes(uuidTokenVersion, randomBoolean()).v1() - ) - ); - - try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { - PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = Authenticator.extractBearerTokenFromHeader(requestContext); - tokenService.tryAuthenticateToken(bearerToken, future); - assertNull(future.get()); - } - } - public void testNonExistingLatestTokenVersion() throws Exception { TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); // mock another random token so that we don't find a token in TokenService#getUserTokenFromId @@ -925,18 +723,11 @@ public void testIndexNotAvailable() throws Exception { return Void.TYPE; }).when(client).get(any(GetRequest.class), anyActionListener()); - final SecurityIndexManager tokensIndex; - if (pre72OldNode != null) { - tokensIndex = securityMainIndex; - when(securityTokensIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false); - when(securityTokensIndex.indexExists()).thenReturn(false); - when(securityTokensIndex.defensiveCopy()).thenReturn(securityTokensIndex); - } else { - tokensIndex = securityTokensIndex; - when(securityMainIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false); - when(securityMainIndex.indexExists()).thenReturn(false); - when(securityMainIndex.defensiveCopy()).thenReturn(securityMainIndex); - } + final SecurityIndexManager tokensIndex = securityTokensIndex; + when(securityMainIndex.isAvailable(SecurityIndexManager.Availability.PRIMARY_SHARDS)).thenReturn(false); + when(securityMainIndex.indexExists()).thenReturn(false); + when(securityMainIndex.defensiveCopy()).thenReturn(securityMainIndex); + try (ThreadContext.StoredContext ignore = requestContext.newStoredContextPreservingResponseHeaders()) { PlainActionFuture future = new PlainActionFuture<>(); final SecureString bearerToken3 = Authenticator.extractBearerTokenFromHeader(requestContext); @@ -988,7 +779,6 @@ public void testGetAuthenticationWorksWithExpiredUserToken() throws Exception { } public void testSupersedingTokenEncryption() throws Exception { - assumeTrue("Superseding tokens are only created in post 7.2 clusters", pre72OldNode == null); TokenService tokenService = createTokenService(tokenServiceEnabledSettings, Clock.systemUTC()); Authentication authentication = AuthenticationTests.randomAuthentication(null, null); PlainActionFuture tokenFuture = new PlainActionFuture<>(); @@ -1023,13 +813,11 @@ public void testSupersedingTokenEncryption() throws Exception { authentication, tokenFuture ); - if (version.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) { - // previous versions serialized the access token encrypted and the cipher text was different each time (due to different IVs) - assertThat( - tokenService.prependVersionAndEncodeAccessToken(version, newTokenBytes.v1()), - equalTo(tokenFuture.get().getAccessToken()) - ); - } + + assertThat( + tokenService.prependVersionAndEncodeAccessToken(version, newTokenBytes.v1()), + equalTo(tokenFuture.get().getAccessToken()) + ); assertThat( TokenService.prependVersionAndEncodeRefreshToken(version, newTokenBytes.v2()), equalTo(tokenFuture.get().getRefreshToken()) @@ -1158,10 +946,8 @@ public static String tokenDocIdFromAccessTokenBytes(byte[] accessTokenBytes, Tra MessageDigest userTokenIdDigest = sha256(); userTokenIdDigest.update(accessTokenBytes, RAW_TOKEN_BYTES_LENGTH, RAW_TOKEN_DOC_ID_BYTES_LENGTH); return Base64.getUrlEncoder().withoutPadding().encodeToString(userTokenIdDigest.digest()); - } else if (tokenVersion.onOrAfter(TokenService.VERSION_ACCESS_TOKENS_AS_UUIDS)) { - return TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes)); } else { - return Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes); + return TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(accessTokenBytes)); } } @@ -1178,12 +964,9 @@ private void mockTokenForRefreshToken( if (userToken.getTransportVersion().onOrAfter(VERSION_GET_TOKEN_DOC_FOR_REFRESH)) { storedAccessToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(accessTokenBytes)); storedRefreshToken = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256().digest(refreshTokenBytes)); - } else if (userToken.getTransportVersion().onOrAfter(TokenService.VERSION_HASHED_TOKENS)) { - storedAccessToken = null; - storedRefreshToken = TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes)); } else { storedAccessToken = null; - storedRefreshToken = Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes); + storedRefreshToken = TokenService.hashTokenString(Base64.getUrlEncoder().withoutPadding().encodeToString(refreshTokenBytes)); } final RealmRef realmRef = new RealmRef( refreshTokenStatus == null ? randomAlphaOfLength(6) : refreshTokenStatus.getAssociatedRealm(), From 2e9ff9ae66bff80df6e8a2149fdef818a6efe573 Mon Sep 17 00:00:00 2001 From: Lola Date: Mon, 9 Dec 2024 05:06:46 -0500 Subject: [PATCH 07/39] [Cloud Security]Fix Cloud Security Package indices' deletion step error for ilm policy (#116982) * add ilm deletion step permission for the findings index * add back logs-endpoint index * fix tests for reserved role * fix linting issue --- .../store/KibanaOwnedReservedRoleDescriptors.java | 2 ++ .../authz/store/ReservedRolesStoreTests.java | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/KibanaOwnedReservedRoleDescriptors.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/KibanaOwnedReservedRoleDescriptors.java index cc589b53eaa1..5e19b26b8f4d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/KibanaOwnedReservedRoleDescriptors.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/KibanaOwnedReservedRoleDescriptors.java @@ -331,6 +331,8 @@ static RoleDescriptor kibanaSystem(String name) { ".logs-endpoint.diagnostic.collection-*", "logs-apm-*", "logs-apm.*-*", + "logs-cloud_security_posture.findings-*", + "logs-cloud_security_posture.vulnerabilities-*", "metrics-apm-*", "metrics-apm.*-*", "traces-apm-*", diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index eeffa1db5485..b69b0ece8996 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -1586,10 +1586,8 @@ public void testKibanaSystemRole() { final IndexAbstraction indexAbstraction = mockIndexAbstraction(cspIndex); assertThat(kibanaRole.indices().allowedIndicesMatcher("indices:foo").test(indexAbstraction), is(false)); assertThat(kibanaRole.indices().allowedIndicesMatcher("indices:bar").test(indexAbstraction), is(false)); - assertThat( - kibanaRole.indices().allowedIndicesMatcher(TransportDeleteIndexAction.TYPE.name()).test(indexAbstraction), - is(false) - ); + // Ensure privileges necessary for ILM policies in Cloud Security Posture Package + assertThat(kibanaRole.indices().allowedIndicesMatcher(TransportDeleteIndexAction.TYPE.name()).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(indexAbstraction), is(true)); assertThat( kibanaRole.indices().allowedIndicesMatcher(TransportCreateIndexAction.TYPE.name()).test(indexAbstraction), @@ -1613,10 +1611,9 @@ public void testKibanaSystemRole() { final IndexAbstraction indexAbstraction = mockIndexAbstraction(cspIndex); assertThat(kibanaRole.indices().allowedIndicesMatcher("indices:foo").test(indexAbstraction), is(false)); assertThat(kibanaRole.indices().allowedIndicesMatcher("indices:bar").test(indexAbstraction), is(false)); - assertThat( - kibanaRole.indices().allowedIndicesMatcher(TransportDeleteIndexAction.TYPE.name()).test(indexAbstraction), - is(false) - ); + // Ensure privileges necessary for ILM policies in Cloud Security Posture Package + assertThat(kibanaRole.indices().allowedIndicesMatcher(TransportDeleteIndexAction.TYPE.name()).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(TransportDeleteIndexAction.TYPE.name()).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(indexAbstraction), is(true)); assertThat( kibanaRole.indices().allowedIndicesMatcher(TransportCreateIndexAction.TYPE.name()).test(indexAbstraction), @@ -1710,6 +1707,7 @@ public void testKibanaSystemRole() { kibanaRole.indices().allowedIndicesMatcher("indices:monitor/" + randomAlphaOfLengthBetween(3, 8)).test(indexAbstraction), is(true) ); + }); // cloud_defend From 6bb0799893f4ee4e6afa7094346fbc51d5203538 Mon Sep 17 00:00:00 2001 From: kosabogi <105062005+kosabogi@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:37:41 +0100 Subject: [PATCH 08/39] Updates h7 and h8 formatting (#118132) --- .../connector/docs/connectors-box.asciidoc | 12 +++---- .../connectors-content-extraction.asciidoc | 4 +-- .../docs/connectors-dropbox.asciidoc | 16 +++++----- .../connector/docs/connectors-github.asciidoc | 20 ++++++------ .../connector/docs/connectors-ms-sql.asciidoc | 12 +++---- .../docs/connectors-network-drive.asciidoc | 16 +++++----- .../connector/docs/connectors-notion.asciidoc | 32 +++++++++---------- .../docs/connectors-onedrive.asciidoc | 24 +++++++------- .../docs/connectors-postgresql.asciidoc | 24 +++++++------- .../connector/docs/connectors-s3.asciidoc | 4 +-- .../docs/connectors-salesforce.asciidoc | 12 +++---- .../docs/connectors-servicenow.asciidoc | 12 +++---- .../connectors-sharepoint-online.asciidoc | 16 +++++----- 13 files changed, 102 insertions(+), 102 deletions(-) diff --git a/docs/reference/connector/docs/connectors-box.asciidoc b/docs/reference/connector/docs/connectors-box.asciidoc index 07e4308d67c2..3e95f15d16cc 100644 --- a/docs/reference/connector/docs/connectors-box.asciidoc +++ b/docs/reference/connector/docs/connectors-box.asciidoc @@ -54,7 +54,7 @@ For additional operations, see <>. ====== Box Free Account [discrete#es-connectors-box-create-oauth-custom-app] -======= Create Box User Authentication (OAuth 2.0) Custom App +*Create Box User Authentication (OAuth 2.0) Custom App* You'll need to create an OAuth app in the Box developer console by following these steps: @@ -64,7 +64,7 @@ You'll need to create an OAuth app in the Box developer console by following the 4. Once the app is created, *Client ID* and *Client secret* values are available in the configuration tab. Keep these handy. [discrete#es-connectors-box-connector-generate-a-refresh-token] -======= Generate a refresh Token +*Generate a refresh Token* To generate a refresh token, follow these steps: @@ -97,7 +97,7 @@ Save the refresh token from the response. You'll need this for the connector con ====== Box Enterprise Account [discrete#es-connectors-box-connector-create-box-server-authentication-client-credentials-grant-custom-app] -======= Create Box Server Authentication (Client Credentials Grant) Custom App +*Create Box Server Authentication (Client Credentials Grant) Custom App* 1. Register a new app in the https://app.box.com/developers/console[Box dev console] with custom App and select Server Authentication (Client Credentials Grant). 2. Check following permissions: @@ -224,7 +224,7 @@ For additional operations, see <>. ====== Box Free Account [discrete#es-connectors-box-client-create-oauth-custom-app] -======= Create Box User Authentication (OAuth 2.0) Custom App +*Create Box User Authentication (OAuth 2.0) Custom App* You'll need to create an OAuth app in the Box developer console by following these steps: @@ -234,7 +234,7 @@ You'll need to create an OAuth app in the Box developer console by following the 4. Once the app is created, *Client ID* and *Client secret* values are available in the configuration tab. Keep these handy. [discrete#es-connectors-box-client-connector-generate-a-refresh-token] -======= Generate a refresh Token +*Generate a refresh Token* To generate a refresh token, follow these steps: @@ -267,7 +267,7 @@ Save the refresh token from the response. You'll need this for the connector con ====== Box Enterprise Account [discrete#es-connectors-box-client-connector-create-box-server-authentication-client-credentials-grant-custom-app] -======= Create Box Server Authentication (Client Credentials Grant) Custom App +*Create Box Server Authentication (Client Credentials Grant) Custom App* 1. Register a new app in the https://app.box.com/developers/console[Box dev console] with custom App and select Server Authentication (Client Credentials Grant). 2. Check following permissions: diff --git a/docs/reference/connector/docs/connectors-content-extraction.asciidoc b/docs/reference/connector/docs/connectors-content-extraction.asciidoc index 5d2a9550a7c3..a87d38c9bf53 100644 --- a/docs/reference/connector/docs/connectors-content-extraction.asciidoc +++ b/docs/reference/connector/docs/connectors-content-extraction.asciidoc @@ -183,7 +183,7 @@ Be aware that the self-managed connector will download files with randomized fil For that reason, we recommend using a dedicated directory for self-hosted extraction. [discrete#es-connectors-content-extraction-data-extraction-service-file-pointers-configuration-example] -======= Example +*Example* 1. For this example, we will be using `/app/files` as both our local directory and our container directory. When you run the extraction service docker container, you can mount the directory as a volume using the command-line option `-v /app/files:/app/files`. @@ -228,7 +228,7 @@ When using self-hosted extraction from a dockerized self-managed connector, ther * The self-managed connector and the extraction service will also need to share a volume. You can decide what directory inside these docker containers the volume will be mounted onto, but the directory must be the same for both docker containers. [discrete#es-connectors-content-extraction-data-extraction-service-file-pointers-configuration-dockerized-example] -======= Example +*Example* 1. First, set up a volume for the two docker containers to share. This will be where files are downloaded into and then extracted from. diff --git a/docs/reference/connector/docs/connectors-dropbox.asciidoc b/docs/reference/connector/docs/connectors-dropbox.asciidoc index 1f80a0ab4e95..295b7e293662 100644 --- a/docs/reference/connector/docs/connectors-dropbox.asciidoc +++ b/docs/reference/connector/docs/connectors-dropbox.asciidoc @@ -190,7 +190,7 @@ When both are provided, priority is given to `file_categories`. We have some examples below for illustration. [discrete#es-connectors-dropbox-sync-rules-advanced-example-1] -======= Example: Query only +*Example: Query only* [source,js] ---- @@ -206,7 +206,7 @@ We have some examples below for illustration. // NOTCONSOLE [discrete#es-connectors-dropbox-sync-rules-advanced-example-2] -======= Example: Query with file extension filter +*Example: Query with file extension filter* [source,js] ---- @@ -225,7 +225,7 @@ We have some examples below for illustration. // NOTCONSOLE [discrete#es-connectors-dropbox-sync-rules-advanced-example-3] -======= Example: Query with file category filter +*Example: Query with file category filter* [source,js] ---- @@ -248,7 +248,7 @@ We have some examples below for illustration. // NOTCONSOLE [discrete#es-connectors-dropbox-sync-rules-advanced-limitations] -======= Limitations +*Limitations* * Content extraction is not supported for Dropbox *Paper* files when advanced sync rules are enabled. @@ -474,7 +474,7 @@ When both are provided, priority is given to `file_categories`. We have some examples below for illustration. [discrete#es-connectors-dropbox-client-sync-rules-advanced-example-1] -======= Example: Query only +*Example: Query only* [source,js] ---- @@ -490,7 +490,7 @@ We have some examples below for illustration. // NOTCONSOLE [discrete#es-connectors-dropbox-client-sync-rules-advanced-example-2] -======= Example: Query with file extension filter +*Example: Query with file extension filter* [source,js] ---- @@ -509,7 +509,7 @@ We have some examples below for illustration. // NOTCONSOLE [discrete#es-connectors-dropbox-client-sync-rules-advanced-example-3] -======= Example: Query with file category filter +*Example: Query with file category filter* [source,js] ---- @@ -532,7 +532,7 @@ We have some examples below for illustration. // NOTCONSOLE [discrete#es-connectors-dropbox-client-sync-rules-advanced-limitations] -======= Limitations +*Limitations* * Content extraction is not supported for Dropbox *Paper* files when advanced sync rules are enabled. diff --git a/docs/reference/connector/docs/connectors-github.asciidoc b/docs/reference/connector/docs/connectors-github.asciidoc index aa683e4bb082..df577d83e812 100644 --- a/docs/reference/connector/docs/connectors-github.asciidoc +++ b/docs/reference/connector/docs/connectors-github.asciidoc @@ -210,7 +210,7 @@ Advanced sync rules are defined through a source-specific DSL JSON snippet. The following sections provide examples of advanced sync rules for this connector. [discrete#es-connectors-github-sync-rules-advanced-branch] -======= Indexing document and files based on branch name configured via branch key +*Indexing document and files based on branch name configured via branch key* [source,js] ---- @@ -226,7 +226,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-github-sync-rules-advanced-issue-key] -======= Indexing document based on issue query related to bugs via issue key +*Indexing document based on issue query related to bugs via issue key* [source,js] ---- @@ -242,7 +242,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-github-sync-rules-advanced-pr-key] -======= Indexing document based on PR query related to open PR's via PR key +*Indexing document based on PR query related to open PR's via PR key* [source,js] ---- @@ -258,7 +258,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-github-sync-rules-advanced-issue-query-branch-name] -======= Indexing document and files based on queries and branch name +*Indexing document and files based on queries and branch name* [source,js] ---- @@ -283,7 +283,7 @@ Check the Elasticsearch index for the actual document count. ==== [discrete#es-connectors-github-sync-rules-advanced-overlapping] -======= Advanced rules for overlapping +*Advanced rules for overlapping* [source,js] ---- @@ -550,7 +550,7 @@ Advanced sync rules are defined through a source-specific DSL JSON snippet. The following sections provide examples of advanced sync rules for this connector. [discrete#es-connectors-github-client-sync-rules-advanced-branch] -======= Indexing document and files based on branch name configured via branch key +*Indexing document and files based on branch name configured via branch key* [source,js] ---- @@ -566,7 +566,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-github-client-sync-rules-advanced-issue-key] -======= Indexing document based on issue query related to bugs via issue key +*Indexing document based on issue query related to bugs via issue key* [source,js] ---- @@ -582,7 +582,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-github-client-sync-rules-advanced-pr-key] -======= Indexing document based on PR query related to open PR's via PR key +*Indexing document based on PR query related to open PR's via PR key* [source,js] ---- @@ -598,7 +598,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-github-client-sync-rules-advanced-issue-query-branch-name] -======= Indexing document and files based on queries and branch name +*Indexing document and files based on queries and branch name* [source,js] ---- @@ -623,7 +623,7 @@ Check the Elasticsearch index for the actual document count. ==== [discrete#es-connectors-github-client-sync-rules-advanced-overlapping] -======= Advanced rules for overlapping +*Advanced rules for overlapping* [source,js] ---- diff --git a/docs/reference/connector/docs/connectors-ms-sql.asciidoc b/docs/reference/connector/docs/connectors-ms-sql.asciidoc index 47fb282b1687..d706af8ca804 100644 --- a/docs/reference/connector/docs/connectors-ms-sql.asciidoc +++ b/docs/reference/connector/docs/connectors-ms-sql.asciidoc @@ -196,7 +196,7 @@ Here are a few examples of advanced sync rules for this connector. ==== [discrete#es-connectors-ms-sql-sync-rules-advanced-queries] -======= Example: Two queries +*Example: Two queries* These rules fetch all records from both the `employee` and `customer` tables. The data from these tables will be synced separately to Elasticsearch. @@ -220,7 +220,7 @@ These rules fetch all records from both the `employee` and `customer` tables. Th // NOTCONSOLE [discrete#es-connectors-ms-sql-sync-rules-example-one-where] -======= Example: One WHERE query +*Example: One WHERE query* This rule fetches only the records from the `employee` table where the `emp_id` is greater than 5. Only these filtered records will be synced to Elasticsearch. @@ -236,7 +236,7 @@ This rule fetches only the records from the `employee` table where the `emp_id` // NOTCONSOLE [discrete#es-connectors-ms-sql-sync-rules-example-one-join] -======= Example: One JOIN query +*Example: One JOIN query* This rule fetches records by performing an INNER JOIN between the `employee` and `customer` tables on the condition that the `emp_id` in `employee` matches the `c_id` in `customer`. The result of this combined data will be synced to Elasticsearch. @@ -484,7 +484,7 @@ Here are a few examples of advanced sync rules for this connector. ==== [discrete#es-connectors-ms-sql-client-sync-rules-advanced-queries] -======= Example: Two queries +*Example: Two queries* These rules fetch all records from both the `employee` and `customer` tables. The data from these tables will be synced separately to Elasticsearch. @@ -508,7 +508,7 @@ These rules fetch all records from both the `employee` and `customer` tables. Th // NOTCONSOLE [discrete#es-connectors-ms-sql-client-sync-rules-example-one-where] -======= Example: One WHERE query +*Example: One WHERE query* This rule fetches only the records from the `employee` table where the `emp_id` is greater than 5. Only these filtered records will be synced to Elasticsearch. @@ -524,7 +524,7 @@ This rule fetches only the records from the `employee` table where the `emp_id` // NOTCONSOLE [discrete#es-connectors-ms-sql-client-sync-rules-example-one-join] -======= Example: One JOIN query +*Example: One JOIN query* This rule fetches records by performing an INNER JOIN between the `employee` and `customer` tables on the condition that the `emp_id` in `employee` matches the `c_id` in `customer`. The result of this combined data will be synced to Elasticsearch. diff --git a/docs/reference/connector/docs/connectors-network-drive.asciidoc b/docs/reference/connector/docs/connectors-network-drive.asciidoc index 91c9d3b28c38..909e3440c9f0 100644 --- a/docs/reference/connector/docs/connectors-network-drive.asciidoc +++ b/docs/reference/connector/docs/connectors-network-drive.asciidoc @@ -174,7 +174,7 @@ Advanced sync rules for this connector use *glob patterns*. The following sections provide examples of advanced sync rules for this connector. [discrete#es-connectors-network-drive-indexing-files-and-folders-recursively-within-folders] -======= Indexing files and folders recursively within folders +*Indexing files and folders recursively within folders* [source,js] ---- @@ -190,7 +190,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-network-drive-indexing-files-and-folders-directly-inside-folder] -======= Indexing files and folders directly inside folder +*Indexing files and folders directly inside folder* [source,js] ---- @@ -203,7 +203,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-network-drive-indexing-files-and-folders-directly-inside-a-set-of-folders] -======= Indexing files and folders directly inside a set of folders +*Indexing files and folders directly inside a set of folders* [source,js] ---- @@ -216,7 +216,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-network-drive-excluding-files-and-folders-that-match-a-pattern] -======= Excluding files and folders that match a pattern +*Excluding files and folders that match a pattern* [source,js] ---- @@ -432,7 +432,7 @@ Advanced sync rules for this connector use *glob patterns*. The following sections provide examples of advanced sync rules for this connector. [discrete#es-connectors-network-drive-client-indexing-files-and-folders-recursively-within-folders] -======= Indexing files and folders recursively within folders +*Indexing files and folders recursively within folders* [source,js] ---- @@ -448,7 +448,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-network-drive-client-indexing-files-and-folders-directly-inside-folder] -======= Indexing files and folders directly inside folder +*Indexing files and folders directly inside folder* [source,js] ---- @@ -461,7 +461,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-network-drive-client-indexing-files-and-folders-directly-inside-a-set-of-folders] -======= Indexing files and folders directly inside a set of folders +*Indexing files and folders directly inside a set of folders* [source,js] ---- @@ -474,7 +474,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-network-drive-client-excluding-files-and-folders-that-match-a-pattern] -======= Excluding files and folders that match a pattern +*Excluding files and folders that match a pattern* [source,js] ---- diff --git a/docs/reference/connector/docs/connectors-notion.asciidoc b/docs/reference/connector/docs/connectors-notion.asciidoc index 2d7a71bff20d..7c08c5d81e03 100644 --- a/docs/reference/connector/docs/connectors-notion.asciidoc +++ b/docs/reference/connector/docs/connectors-notion.asciidoc @@ -140,7 +140,7 @@ Advanced sync rules for Notion take the following parameters: ====== Examples [discrete] -======= Example 1 +*Example 1* Indexing every page where the title contains `Demo Page`: @@ -160,7 +160,7 @@ Indexing every page where the title contains `Demo Page`: // NOTCONSOLE [discrete] -======= Example 2 +*Example 2* Indexing every database where the title contains `Demo Database`: @@ -180,7 +180,7 @@ Indexing every database where the title contains `Demo Database`: // NOTCONSOLE [discrete] -======= Example 3 +*Example 3* Indexing every database where the title contains `Demo Database` and every page where the title contains `Demo Page`: @@ -206,7 +206,7 @@ Indexing every database where the title contains `Demo Database` and every page // NOTCONSOLE [discrete] -======= Example 4 +*Example 4* Indexing all pages in the workspace: @@ -226,7 +226,7 @@ Indexing all pages in the workspace: // NOTCONSOLE [discrete] -======= Example 5 +*Example 5* Indexing all the pages and databases connected to the workspace: @@ -243,7 +243,7 @@ Indexing all the pages and databases connected to the workspace: // NOTCONSOLE [discrete] -======= Example 6 +*Example 6* Indexing all the rows of a database where the record is `true` for the column `Task completed` and its property(datatype) is a checkbox: @@ -266,7 +266,7 @@ Indexing all the rows of a database where the record is `true` for the column `T // NOTCONSOLE [discrete] -======= Example 7 +*Example 7* Indexing all rows of a specific database: @@ -283,7 +283,7 @@ Indexing all rows of a specific database: // NOTCONSOLE [discrete] -======= Example 8 +*Example 8* Indexing all blocks defined in `searches` and `database_query_filters`: @@ -498,7 +498,7 @@ Advanced sync rules for Notion take the following parameters: ====== Examples [discrete] -======= Example 1 +*Example 1* Indexing every page where the title contains `Demo Page`: @@ -518,7 +518,7 @@ Indexing every page where the title contains `Demo Page`: // NOTCONSOLE [discrete] -======= Example 2 +*Example 2* Indexing every database where the title contains `Demo Database`: @@ -538,7 +538,7 @@ Indexing every database where the title contains `Demo Database`: // NOTCONSOLE [discrete] -======= Example 3 +*Example 3* Indexing every database where the title contains `Demo Database` and every page where the title contains `Demo Page`: @@ -564,7 +564,7 @@ Indexing every database where the title contains `Demo Database` and every page // NOTCONSOLE [discrete] -======= Example 4 +*Example 4* Indexing all pages in the workspace: @@ -584,7 +584,7 @@ Indexing all pages in the workspace: // NOTCONSOLE [discrete] -======= Example 5 +*Example 5* Indexing all the pages and databases connected to the workspace: @@ -601,7 +601,7 @@ Indexing all the pages and databases connected to the workspace: // NOTCONSOLE [discrete] -======= Example 6 +*Example 6* Indexing all the rows of a database where the record is `true` for the column `Task completed` and its property(datatype) is a checkbox: @@ -624,7 +624,7 @@ Indexing all the rows of a database where the record is `true` for the column `T // NOTCONSOLE [discrete] -======= Example 7 +*Example 7* Indexing all rows of a specific database: @@ -641,7 +641,7 @@ Indexing all rows of a specific database: // NOTCONSOLE [discrete] -======= Example 8 +*Example 8* Indexing all blocks defined in `searches` and `database_query_filters`: diff --git a/docs/reference/connector/docs/connectors-onedrive.asciidoc b/docs/reference/connector/docs/connectors-onedrive.asciidoc index 7d1a21aeb78d..44ac96e2ad99 100644 --- a/docs/reference/connector/docs/connectors-onedrive.asciidoc +++ b/docs/reference/connector/docs/connectors-onedrive.asciidoc @@ -160,7 +160,7 @@ A <> is required for advanced sync rul Here are a few examples of advanced sync rules for this connector. [discrete#es-connectors-onedrive-sync-rules-advanced-examples-1] -======= Example 1 +*Example 1* This rule skips indexing for files with `.xlsx` and `.docx` extensions. All other files and folders will be indexed. @@ -176,7 +176,7 @@ All other files and folders will be indexed. // NOTCONSOLE [discrete#es-connectors-onedrive-sync-rules-advanced-examples-2] -======= Example 2 +*Example 2* This rule focuses on indexing files and folders owned by `user1-domain@onmicrosoft.com` and `user2-domain@onmicrosoft.com` but excludes files with `.py` extension. @@ -192,7 +192,7 @@ This rule focuses on indexing files and folders owned by `user1-domain@onmicroso // NOTCONSOLE [discrete#es-connectors-onedrive-sync-rules-advanced-examples-3] -======= Example 3 +*Example 3* This rule indexes only the files and folders directly inside the root folder, excluding any `.md` files. @@ -208,7 +208,7 @@ This rule indexes only the files and folders directly inside the root folder, ex // NOTCONSOLE [discrete#es-connectors-onedrive-sync-rules-advanced-examples-4] -======= Example 4 +*Example 4* This rule indexes files and folders owned by `user1-domain@onmicrosoft.com` and `user3-domain@onmicrosoft.com` that are directly inside the `abc` folder, which is a subfolder of any folder under the `hello` directory in the root. Files with extensions `.pdf` and `.py` are excluded. @@ -225,7 +225,7 @@ This rule indexes files and folders owned by `user1-domain@onmicrosoft.com` and // NOTCONSOLE [discrete#es-connectors-onedrive-sync-rules-advanced-examples-5] -======= Example 5 +*Example 5* This example contains two rules. The first rule indexes all files and folders owned by `user1-domain@onmicrosoft.com` and `user2-domain@onmicrosoft.com`. @@ -245,7 +245,7 @@ The second rule indexes files for all other users, but skips files with a `.py` // NOTCONSOLE [discrete#es-connectors-onedrive-sync-rules-advanced-examples-6] -======= Example 6 +*Example 6* This example contains two rules. The first rule indexes all files owned by `user1-domain@onmicrosoft.com` and `user2-domain@onmicrosoft.com`, excluding `.md` files. @@ -449,7 +449,7 @@ A <> is required for advanced sync rul Here are a few examples of advanced sync rules for this connector. [discrete#es-connectors-onedrive-client-sync-rules-advanced-examples-1] -======= Example 1 +*Example 1* This rule skips indexing for files with `.xlsx` and `.docx` extensions. All other files and folders will be indexed. @@ -465,7 +465,7 @@ All other files and folders will be indexed. // NOTCONSOLE [discrete#es-connectors-onedrive-client-sync-rules-advanced-examples-2] -======= Example 2 +*Example 2* This rule focuses on indexing files and folders owned by `user1-domain@onmicrosoft.com` and `user2-domain@onmicrosoft.com` but excludes files with `.py` extension. @@ -481,7 +481,7 @@ This rule focuses on indexing files and folders owned by `user1-domain@onmicroso // NOTCONSOLE [discrete#es-connectors-onedrive-client-sync-rules-advanced-examples-3] -======= Example 3 +*Example 3* This rule indexes only the files and folders directly inside the root folder, excluding any `.md` files. @@ -497,7 +497,7 @@ This rule indexes only the files and folders directly inside the root folder, ex // NOTCONSOLE [discrete#es-connectors-onedrive-client-sync-rules-advanced-examples-4] -======= Example 4 +*Example 4* This rule indexes files and folders owned by `user1-domain@onmicrosoft.com` and `user3-domain@onmicrosoft.com` that are directly inside the `abc` folder, which is a subfolder of any folder under the `hello` directory in the root. Files with extensions `.pdf` and `.py` are excluded. @@ -514,7 +514,7 @@ This rule indexes files and folders owned by `user1-domain@onmicrosoft.com` and // NOTCONSOLE [discrete#es-connectors-onedrive-client-sync-rules-advanced-examples-5] -======= Example 5 +*Example 5* This example contains two rules. The first rule indexes all files and folders owned by `user1-domain@onmicrosoft.com` and `user2-domain@onmicrosoft.com`. @@ -534,7 +534,7 @@ The second rule indexes files for all other users, but skips files with a `.py` // NOTCONSOLE [discrete#es-connectors-onedrive-client-sync-rules-advanced-examples-6] -======= Example 6 +*Example 6* This example contains two rules. The first rule indexes all files owned by `user1-domain@onmicrosoft.com` and `user2-domain@onmicrosoft.com`, excluding `.md` files. diff --git a/docs/reference/connector/docs/connectors-postgresql.asciidoc b/docs/reference/connector/docs/connectors-postgresql.asciidoc index 1fe28f867337..aa6cb7f29e63 100644 --- a/docs/reference/connector/docs/connectors-postgresql.asciidoc +++ b/docs/reference/connector/docs/connectors-postgresql.asciidoc @@ -188,7 +188,7 @@ Advanced sync rules are defined through a source-specific DSL JSON snippet. Here is some example data that will be used in the following examples. [discrete#connectors-postgresql-sync-rules-advanced-example-data-1] -======= `employee` table +*`employee` table* [cols="3*", options="header"] |=== @@ -199,7 +199,7 @@ Here is some example data that will be used in the following examples. |=== [discrete#connectors-postgresql-sync-rules-advanced-example-2] -======= `customer` table +*`customer` table* [cols="3*", options="header"] |=== @@ -213,7 +213,7 @@ Here is some example data that will be used in the following examples. ====== Advanced sync rules examples [discrete#connectors-postgresql-sync-rules-advanced-examples-1] -======= Multiple table queries +*Multiple table queries* [source,js] ---- @@ -235,7 +235,7 @@ Here is some example data that will be used in the following examples. // NOTCONSOLE [discrete#connectors-postgresql-sync-rules-advanced-examples-1-id-columns] -======= Multiple table queries with `id_columns` +*Multiple table queries with `id_columns`* In 8.15.0, we added a new optional `id_columns` field in our advanced sync rules for the PostgreSQL connector. Use the `id_columns` field to ingest tables which do not have a primary key. Include the names of unique fields so that the connector can use them to generate unique IDs for documents. @@ -264,7 +264,7 @@ Use the `id_columns` field to ingest tables which do not have a primary key. Inc This example uses the `id_columns` field to specify the unique fields `emp_id` and `c_id` for the `employee` and `customer` tables, respectively. [discrete#connectors-postgresql-sync-rules-advanced-examples-2] -======= Filtering data with `WHERE` clause +*Filtering data with `WHERE` clause* [source,js] ---- @@ -278,7 +278,7 @@ This example uses the `id_columns` field to specify the unique fields `emp_id` a // NOTCONSOLE [discrete#connectors-postgresql-sync-rules-advanced-examples-3] -======= `JOIN` operations +*`JOIN` operations* [source,js] ---- @@ -494,7 +494,7 @@ Advanced sync rules are defined through a source-specific DSL JSON snippet. Here is some example data that will be used in the following examples. [discrete#es-connectors-postgresql-client-sync-rules-advanced-example-data-1] -======= `employee` table +*`employee` table* [cols="3*", options="header"] |=== @@ -505,7 +505,7 @@ Here is some example data that will be used in the following examples. |=== [discrete#es-connectors-postgresql-client-sync-rules-advanced-example-2] -======= `customer` table +*`customer` table* [cols="3*", options="header"] |=== @@ -519,7 +519,7 @@ Here is some example data that will be used in the following examples. ====== Advanced sync rules examples [discrete#es-connectors-postgresql-client-sync-rules-advanced-examples-1] -======== Multiple table queries +*Multiple table queries* [source,js] ---- @@ -541,7 +541,7 @@ Here is some example data that will be used in the following examples. // NOTCONSOLE [discrete#es-connectors-postgresql-client-sync-rules-advanced-examples-1-id-columns] -======== Multiple table queries with `id_columns` +*Multiple table queries with `id_columns`* In 8.15.0, we added a new optional `id_columns` field in our advanced sync rules for the PostgreSQL connector. Use the `id_columns` field to ingest tables which do not have a primary key. Include the names of unique fields so that the connector can use them to generate unique IDs for documents. @@ -570,7 +570,7 @@ Use the `id_columns` field to ingest tables which do not have a primary key. Inc This example uses the `id_columns` field to specify the unique fields `emp_id` and `c_id` for the `employee` and `customer` tables, respectively. [discrete#es-connectors-postgresql-client-sync-rules-advanced-examples-2] -======== Filtering data with `WHERE` clause +*Filtering data with `WHERE` clause* [source,js] ---- @@ -584,7 +584,7 @@ This example uses the `id_columns` field to specify the unique fields `emp_id` a // NOTCONSOLE [discrete#es-connectors-postgresql-client-sync-rules-advanced-examples-3] -======== `JOIN` operations +*`JOIN` operations* [source,js] ---- diff --git a/docs/reference/connector/docs/connectors-s3.asciidoc b/docs/reference/connector/docs/connectors-s3.asciidoc index b4d08d388463..90c070f7b804 100644 --- a/docs/reference/connector/docs/connectors-s3.asciidoc +++ b/docs/reference/connector/docs/connectors-s3.asciidoc @@ -118,7 +118,7 @@ The connector will fetch file and folder data that matches the string. Defaults to `""` (syncs all bucket objects). [discrete#es-connectors-s3-sync-rules-advanced-examples] -======= Advanced sync rules examples +*Advanced sync rules examples* *Fetching files and folders recursively by prefix* @@ -336,7 +336,7 @@ The connector will fetch file and folder data that matches the string. Defaults to `""` (syncs all bucket objects). [discrete#es-connectors-s3-client-sync-rules-advanced-examples] -======= Advanced sync rules examples +*Advanced sync rules examples* *Fetching files and folders recursively by prefix* diff --git a/docs/reference/connector/docs/connectors-salesforce.asciidoc b/docs/reference/connector/docs/connectors-salesforce.asciidoc index 3676f7663089..c640751de92c 100644 --- a/docs/reference/connector/docs/connectors-salesforce.asciidoc +++ b/docs/reference/connector/docs/connectors-salesforce.asciidoc @@ -227,7 +227,7 @@ They take the following parameters: Allowed values are *SOQL* and *SOSL*. [discrete#es-connectors-salesforce-sync-rules-advanced-fetch-query-language] -======= Fetch documents based on the query and language specified +*Fetch documents based on the query and language specified* **Example**: Fetch documents using SOQL query @@ -256,7 +256,7 @@ Allowed values are *SOQL* and *SOSL*. // NOTCONSOLE [discrete#es-connectors-salesforce-sync-rules-advanced-fetch-objects] -======= Fetch standard and custom objects using SOQL and SOSL queries +*Fetch standard and custom objects using SOQL and SOSL queries* **Example**: Fetch documents for standard objects via SOQL and SOSL query. @@ -293,7 +293,7 @@ Allowed values are *SOQL* and *SOSL*. // NOTCONSOLE [discrete#es-connectors-salesforce-sync-rules-advanced-fetch-standard-custom-fields] -======= Fetch documents with standard and custom fields +*Fetch documents with standard and custom fields* **Example**: Fetch documents with all standard and custom fields for Account object. @@ -626,7 +626,7 @@ They take the following parameters: Allowed values are *SOQL* and *SOSL*. [discrete#es-connectors-salesforce-client-sync-rules-advanced-fetch-query-language] -======= Fetch documents based on the query and language specified +*Fetch documents based on the query and language specified* **Example**: Fetch documents using SOQL query @@ -655,7 +655,7 @@ Allowed values are *SOQL* and *SOSL*. // NOTCONSOLE [discrete#es-connectors-salesforce-client-sync-rules-advanced-fetch-objects] -======= Fetch standard and custom objects using SOQL and SOSL queries +*Fetch standard and custom objects using SOQL and SOSL queries* **Example**: Fetch documents for standard objects via SOQL and SOSL query. @@ -692,7 +692,7 @@ Allowed values are *SOQL* and *SOSL*. // NOTCONSOLE [discrete#es-connectors-salesforce-client-sync-rules-advanced-fetch-standard-custom-fields] -======= Fetch documents with standard and custom fields +*Fetch documents with standard and custom fields* **Example**: Fetch documents with all standard and custom fields for Account object. diff --git a/docs/reference/connector/docs/connectors-servicenow.asciidoc b/docs/reference/connector/docs/connectors-servicenow.asciidoc index a02c418f11d7..3dc98ed9a44c 100644 --- a/docs/reference/connector/docs/connectors-servicenow.asciidoc +++ b/docs/reference/connector/docs/connectors-servicenow.asciidoc @@ -167,7 +167,7 @@ Advanced sync rules are defined through a source-specific DSL JSON snippet. The following sections provide examples of advanced sync rules for this connector. [discrete#es-connectors-servicenow-sync-rules-number-incident-service] -======= Indexing document based on incident number for Incident service +*Indexing document based on incident number for Incident service* [source,js] ---- @@ -181,7 +181,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-servicenow-sync-rules-active-false-user-service] -======= Indexing document based on user activity state for User service +*Indexing document based on user activity state for User service* [source,js] ---- @@ -195,7 +195,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-servicenow-sync-rules-author-administrator-knowledge-service] -======= Indexing document based on author name for Knowledge service +*Indexing document based on author name for Knowledge service* [source,js] ---- @@ -407,7 +407,7 @@ Advanced sync rules are defined through a source-specific DSL JSON snippet. The following sections provide examples of advanced sync rules for this connector. [discrete#es-connectors-servicenow-client-sync-rules-number-incident-service] -======= Indexing document based on incident number for Incident service +*Indexing document based on incident number for Incident service* [source,js] ---- @@ -421,7 +421,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-servicenow-client-sync-rules-active-false-user-service] -======= Indexing document based on user activity state for User service +*Indexing document based on user activity state for User service* [source,js] ---- @@ -435,7 +435,7 @@ The following sections provide examples of advanced sync rules for this connecto // NOTCONSOLE [discrete#es-connectors-servicenow-client-sync-rules-author-administrator-knowledge-service] -======= Indexing document based on author name for Knowledge service +*Indexing document based on author name for Knowledge service* [source,js] ---- diff --git a/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc b/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc index 21d0890e436c..02f598c16f63 100644 --- a/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc +++ b/docs/reference/connector/docs/connectors-sharepoint-online.asciidoc @@ -277,7 +277,7 @@ Example: This rule will not extract content of any drive items (files in document libraries) that haven't been modified for 60 days or more. [discrete#es-connectors-sharepoint-online-sync-rules-limitations] -======= Limitations of sync rules with incremental syncs +*Limitations of sync rules with incremental syncs* Changing sync rules after Sharepoint Online content has already been indexed can bring unexpected results, when using <>. @@ -288,7 +288,7 @@ Incremental syncs ensure _updates_ from 3rd-party system, but do not modify exis Let's take a look at several examples where incremental syncs might lead to inconsistent data on your index. [discrete#es-connectors-sharepoint-online-sync-rules-limitations-restrictive-added] -======== Example: Restrictive basic sync rule added after a full sync +*Example: Restrictive basic sync rule added after a full sync* Imagine your Sharepoint Online drive contains the following drive items: @@ -322,7 +322,7 @@ If no files were changed, incremental sync will not receive information about ch After a *full sync*, the index will be updated and files that are excluded by sync rules will be removed. [discrete#es-connectors-sharepoint-online-sync-rules-limitations-restrictive-removed] -======== Example: Restrictive basic sync rules removed after a full sync +*Example: Restrictive basic sync rules removed after a full sync* Imagine that Sharepoint Online drive has the following drive items: @@ -354,7 +354,7 @@ Afterwards, we can remove the filtering rule and run an incremental sync. If no Only a *full sync* will include the items previously ignored by the sync rule. [discrete#es-connectors-sharepoint-online-sync-rules-limitations-restrictive-changed] -======== Example: Advanced sync rules edge case +*Example: Advanced sync rules edge case* Advanced sync rules can be applied to limit which documents will have content extracted. For example, it's possible to set a rule so that documents older than 180 days won't have content extracted. @@ -763,7 +763,7 @@ Example: This rule will not extract content of any drive items (files in document libraries) that haven't been modified for 60 days or more. [discrete#es-connectors-sharepoint-online-client-sync-rules-limitations] -======= Limitations of sync rules with incremental syncs +*Limitations of sync rules with incremental syncs* Changing sync rules after Sharepoint Online content has already been indexed can bring unexpected results, when using <>. @@ -774,7 +774,7 @@ Incremental syncs ensure _updates_ from 3rd-party system, but do not modify exis Let's take a look at several examples where incremental syncs might lead to inconsistent data on your index. [discrete#es-connectors-sharepoint-online-client-sync-rules-limitations-restrictive-added] -======== Example: Restrictive basic sync rule added after a full sync +*Example: Restrictive basic sync rule added after a full sync* Imagine your Sharepoint Online drive contains the following drive items: @@ -808,7 +808,7 @@ If no files were changed, incremental sync will not receive information about ch After a *full sync*, the index will be updated and files that are excluded by sync rules will be removed. [discrete#es-connectors-sharepoint-online-client-sync-rules-limitations-restrictive-removed] -======== Example: Restrictive basic sync rules removed after a full sync +*Example: Restrictive basic sync rules removed after a full sync* Imagine that Sharepoint Online drive has the following drive items: @@ -840,7 +840,7 @@ Afterwards, we can remove the filtering rule and run an incremental sync. If no Only a *full sync* will include the items previously ignored by the sync rule. [discrete#es-connectors-sharepoint-online-client-sync-rules-limitations-restrictive-changed] -======== Example: Advanced sync rules edge case +*Example: Advanced sync rules edge case* Advanced sync rules can be applied to limit which documents will have content extracted. For example, it's possible to set a rule so that documents older than 180 days won't have content extracted. From b4e852a54be436d8b8036da0a0ec4a472d44524a Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Mon, 9 Dec 2024 12:00:12 +0100 Subject: [PATCH 09/39] [TEST] Wait for no pending operations on the index shard (#118244) This fixes testRetryPointInTime which on teardown is looking to assert that the operations in the translog and in the lucene index are the same. Previously we didn't wait for the translog operations to be applied. This changes `assertConsistentHistoryInLuceneIndex` to wait for the pending operations in the translog to be applied. Fixes #117116 --- muted-tests.yml | 3 --- .../elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 4523db7239be..dcaa415a6796 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -157,9 +157,6 @@ tests: - class: org.elasticsearch.xpack.test.rest.XPackRestIT method: test {p0=snapshot/10_basic/Create a source only snapshot and then restore it} issue: https://github.com/elastic/elasticsearch/issues/117295 -- class: org.elasticsearch.xpack.searchablesnapshots.RetrySearchIntegTests - method: testRetryPointInTime - issue: https://github.com/elastic/elasticsearch/issues/117116 - class: org.elasticsearch.xpack.inference.DefaultEndPointsIT method: testInferDeploysDefaultElser issue: https://github.com/elastic/elasticsearch/issues/114913 diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java index 8bc81fef2157..a2bf70bf6e08 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java @@ -128,6 +128,7 @@ protected Collection> nodePlugins() { @After public void assertConsistentHistoryInLuceneIndex() throws Exception { + internalCluster().beforeIndexDeletion(); internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); } From 67ee03411bad5beb7e8000f0b3fcbc8accdf96da Mon Sep 17 00:00:00 2001 From: kanoshiou <73424326+kanoshiou@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:08:48 +0800 Subject: [PATCH 10/39] ESQL: Enable async get to support formatting (#111104) I've updated the listener for GET /_query/async/{id} to EsqlResponseListener, so it now accepts parameters (delimiter, drop_null_columns and format) like the POST /_query API. Additionally, I have added tests to verify the correctness of the code. You can now set the format in the request parameters to specify the return style. Closes #110926 --- docs/changelog/111104.yaml | 6 + .../esql/esql-async-query-get-api.asciidoc | 4 + .../async/AsyncTaskManagementService.java | 2 +- .../xpack/esql/qa/rest/RestEsqlTestCase.java | 203 ++++++++++++++---- .../esql/action/EsqlResponseListener.java | 66 +++--- .../action/RestEsqlGetAsyncResultAction.java | 3 +- .../esql/plugin/EsqlMediaTypeParser.java | 13 +- .../esql/plugin/EsqlMediaTypeParserTests.java | 13 +- 8 files changed, 236 insertions(+), 74 deletions(-) create mode 100644 docs/changelog/111104.yaml diff --git a/docs/changelog/111104.yaml b/docs/changelog/111104.yaml new file mode 100644 index 000000000000..a7dffdd0be22 --- /dev/null +++ b/docs/changelog/111104.yaml @@ -0,0 +1,6 @@ +pr: 111104 +summary: "ESQL: Enable async get to support formatting" +area: ES|QL +type: feature +issues: + - 110926 diff --git a/docs/reference/esql/esql-async-query-get-api.asciidoc b/docs/reference/esql/esql-async-query-get-api.asciidoc index ec68313b2c49..82a6ae5b28b5 100644 --- a/docs/reference/esql/esql-async-query-get-api.asciidoc +++ b/docs/reference/esql/esql-async-query-get-api.asciidoc @@ -39,6 +39,10 @@ parameter is `true`. [[esql-async-query-get-api-query-params]] ==== {api-query-parms-title} +The API accepts the same parameters as the synchronous +<>, along with the following +parameters: + `wait_for_completion_timeout`:: (Optional, <>) Timeout duration to wait for the request to finish. Defaults to no timeout, diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/async/AsyncTaskManagementService.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/async/AsyncTaskManagementService.java index 94bac95b9150..91fdb9c39b6e 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/async/AsyncTaskManagementService.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/async/AsyncTaskManagementService.java @@ -208,7 +208,7 @@ private ActionListener wrapStoringListener( ActionListener listener ) { AtomicReference> exclusiveListener = new AtomicReference<>(listener); - // This is will performed in case of timeout + // This will be performed in case of timeout Scheduler.ScheduledCancellable timeoutHandler = threadPool.schedule(() -> { ActionListener acquiredListener = exclusiveListener.getAndSet(null); if (acquiredListener != null) { diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java index 505ab3adc553..6a8779eef4ef 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java @@ -350,21 +350,21 @@ public void testTextMode() throws IOException { int count = randomIntBetween(0, 100); bulkLoadTestData(count); var builder = requestObjectBuilder().query(fromIndex() + " | keep keyword, integer | sort integer asc | limit 100"); - assertEquals(expectedTextBody("txt", count, null), runEsqlAsTextWithFormat(builder, "txt", null)); + assertEquals(expectedTextBody("txt", count, null), runEsqlAsTextWithFormat(builder, "txt", null, mode)); } public void testCSVMode() throws IOException { int count = randomIntBetween(0, 100); bulkLoadTestData(count); var builder = requestObjectBuilder().query(fromIndex() + " | keep keyword, integer | sort integer asc | limit 100"); - assertEquals(expectedTextBody("csv", count, '|'), runEsqlAsTextWithFormat(builder, "csv", '|')); + assertEquals(expectedTextBody("csv", count, '|'), runEsqlAsTextWithFormat(builder, "csv", '|', mode)); } public void testTSVMode() throws IOException { int count = randomIntBetween(0, 100); bulkLoadTestData(count); var builder = requestObjectBuilder().query(fromIndex() + " | keep keyword, integer | sort integer asc | limit 100"); - assertEquals(expectedTextBody("tsv", count, null), runEsqlAsTextWithFormat(builder, "tsv", null)); + assertEquals(expectedTextBody("tsv", count, null), runEsqlAsTextWithFormat(builder, "tsv", null, mode)); } public void testCSVNoHeaderMode() throws IOException { @@ -1003,53 +1003,35 @@ public static Map runEsqlSync(RequestObjectBuilder requestObject } public static Map runEsqlAsync(RequestObjectBuilder requestObject) throws IOException { - return runEsqlAsync(requestObject, new AssertWarnings.NoWarnings()); + return runEsqlAsync(requestObject, randomBoolean(), new AssertWarnings.NoWarnings()); } static Map runEsql(RequestObjectBuilder requestObject, AssertWarnings assertWarnings, Mode mode) throws IOException { if (mode == ASYNC) { - return runEsqlAsync(requestObject, assertWarnings); + return runEsqlAsync(requestObject, randomBoolean(), assertWarnings); } else { return runEsqlSync(requestObject, assertWarnings); } } public static Map runEsqlSync(RequestObjectBuilder requestObject, AssertWarnings assertWarnings) throws IOException { - requestObject.build(); - Request request = prepareRequest(SYNC); - String mediaType = attachBody(requestObject, request); - - RequestOptions.Builder options = request.getOptions().toBuilder(); - options.setWarningsHandler(WarningsHandler.PERMISSIVE); // We assert the warnings ourselves - options.addHeader("Content-Type", mediaType); - - if (randomBoolean()) { - options.addHeader("Accept", mediaType); - } else { - request.addParameter("format", requestObject.contentType().queryParameter()); - } - request.setOptions(options); + Request request = prepareRequestWithOptions(requestObject, SYNC); HttpEntity entity = performRequest(request, assertWarnings); return entityToMap(entity, requestObject.contentType()); } public static Map runEsqlAsync(RequestObjectBuilder requestObject, AssertWarnings assertWarnings) throws IOException { - addAsyncParameters(requestObject); - requestObject.build(); - Request request = prepareRequest(ASYNC); - String mediaType = attachBody(requestObject, request); - - RequestOptions.Builder options = request.getOptions().toBuilder(); - options.setWarningsHandler(WarningsHandler.PERMISSIVE); // We assert the warnings ourselves - options.addHeader("Content-Type", mediaType); + return runEsqlAsync(requestObject, randomBoolean(), assertWarnings); + } - if (randomBoolean()) { - options.addHeader("Accept", mediaType); - } else { - request.addParameter("format", requestObject.contentType().queryParameter()); - } - request.setOptions(options); + public static Map runEsqlAsync( + RequestObjectBuilder requestObject, + boolean keepOnCompletion, + AssertWarnings assertWarnings + ) throws IOException { + addAsyncParameters(requestObject, keepOnCompletion); + Request request = prepareRequestWithOptions(requestObject, ASYNC); if (shouldLog()) { LOGGER.info("REQUEST={}", request); @@ -1061,7 +1043,7 @@ public static Map runEsqlAsync(RequestObjectBuilder requestObjec Object initialColumns = null; Object initialValues = null; var json = entityToMap(entity, requestObject.contentType()); - checkKeepOnCompletion(requestObject, json); + checkKeepOnCompletion(requestObject, json, keepOnCompletion); String id = (String) json.get("id"); var supportsAsyncHeaders = clusterHasCapability("POST", "/_query", List.of(), List.of("async_query_status_headers")).orElse(false); @@ -1101,7 +1083,7 @@ public static Map runEsqlAsync(RequestObjectBuilder requestObjec // issue a second request to "async get" the results Request getRequest = prepareAsyncGetRequest(id); - getRequest.setOptions(options); + getRequest.setOptions(request.getOptions()); response = performRequest(getRequest); entity = response.getEntity(); } @@ -1119,6 +1101,66 @@ public static Map runEsqlAsync(RequestObjectBuilder requestObjec return removeAsyncProperties(result); } + public void testAsyncGetWithoutContentType() throws IOException { + int count = randomIntBetween(0, 100); + bulkLoadTestData(count); + var requestObject = requestObjectBuilder().query(fromIndex() + " | keep keyword, integer | sort integer asc | limit 100"); + + addAsyncParameters(requestObject, true); + Request request = prepareRequestWithOptions(requestObject, ASYNC); + + if (shouldLog()) { + LOGGER.info("REQUEST={}", request); + } + + Response response = performRequest(request); + HttpEntity entity = response.getEntity(); + + var json = entityToMap(entity, requestObject.contentType()); + checkKeepOnCompletion(requestObject, json, true); + String id = (String) json.get("id"); + // results won't be returned since keepOnCompletion is true + assertThat(id, is(not(emptyOrNullString()))); + + // issue an "async get" request with no Content-Type + Request getRequest = prepareAsyncGetRequest(id); + response = performRequest(getRequest); + entity = response.getEntity(); + var result = entityToMap(entity, XContentType.JSON); + + ListMatcher values = matchesList(); + for (int i = 0; i < count; i++) { + values = values.item(matchesList().item("keyword" + i).item(i)); + } + assertMap( + result, + matchesMap().entry( + "columns", + matchesList().item(matchesMap().entry("name", "keyword").entry("type", "keyword")) + .item(matchesMap().entry("name", "integer").entry("type", "integer")) + ).entry("values", values).entry("took", greaterThanOrEqualTo(0)).entry("id", id).entry("is_running", false) + ); + + } + + static Request prepareRequestWithOptions(RequestObjectBuilder requestObject, Mode mode) throws IOException { + requestObject.build(); + Request request = prepareRequest(mode); + String mediaType = attachBody(requestObject, request); + + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.setWarningsHandler(WarningsHandler.PERMISSIVE); // We assert the warnings ourselves + options.addHeader("Content-Type", mediaType); + + if (randomBoolean()) { + options.addHeader("Accept", mediaType); + } else { + request.addParameter("format", requestObject.contentType().queryParameter()); + } + request.setOptions(options); + return request; + } + // Removes async properties, otherwise consuming assertions would need to handle sync and async differences static Map removeAsyncProperties(Map map) { Map copy = new HashMap<>(map); @@ -1139,17 +1181,20 @@ protected static Map entityToMap(HttpEntity entity, XContentType } } - static void addAsyncParameters(RequestObjectBuilder requestObject) throws IOException { + static void addAsyncParameters(RequestObjectBuilder requestObject, boolean keepOnCompletion) throws IOException { // deliberately short in order to frequently trigger return without results requestObject.waitForCompletion(TimeValue.timeValueNanos(randomIntBetween(1, 100))); - requestObject.keepOnCompletion(randomBoolean()); + requestObject.keepOnCompletion(keepOnCompletion); requestObject.keepAlive(TimeValue.timeValueDays(randomIntBetween(1, 10))); } // If keep_on_completion is set then an id must always be present, regardless of the value of any other property. - static void checkKeepOnCompletion(RequestObjectBuilder requestObject, Map json) { + static void checkKeepOnCompletion(RequestObjectBuilder requestObject, Map json, boolean keepOnCompletion) { if (requestObject.keepOnCompletion()) { + assertTrue(keepOnCompletion); assertThat((String) json.get("id"), not(emptyOrNullString())); + } else { + assertFalse(keepOnCompletion); } } @@ -1167,14 +1212,19 @@ static void deleteNonExistent(Request request) throws IOException { assertEquals(404, response.getStatusLine().getStatusCode()); } - static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String format, @Nullable Character delimiter) throws IOException { - Request request = prepareRequest(SYNC); + static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String format, @Nullable Character delimiter, Mode mode) + throws IOException { + Request request = prepareRequest(mode); + if (mode == ASYNC) { + addAsyncParameters(builder, randomBoolean()); + } String mediaType = attachBody(builder.build(), request); RequestOptions.Builder options = request.getOptions().toBuilder(); options.addHeader("Content-Type", mediaType); - if (randomBoolean()) { + boolean addParam = randomBoolean(); + if (addParam) { request.addParameter("format", format); } else { switch (format) { @@ -1188,8 +1238,75 @@ static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String forma } request.setOptions(options); - HttpEntity entity = performRequest(request, new AssertWarnings.NoWarnings()); - return Streams.copyToString(new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8)); + if (shouldLog()) { + LOGGER.info("REQUEST={}", request); + } + + Response response = performRequest(request); + HttpEntity entity = assertWarnings(response, new AssertWarnings.NoWarnings()); + + // get the content, it could be empty because the request might have not completed + String initialValue = Streams.copyToString(new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8)); + String id = response.getHeader("X-Elasticsearch-Async-Id"); + + if (mode == SYNC) { + assertThat(id, is(emptyOrNullString())); + return initialValue; + } + + if (id == null) { + // no id returned from an async call, must have completed immediately and without keep_on_completion + assertThat(builder.keepOnCompletion(), either(nullValue()).or(is(false))); + assertNull(response.getHeader("is_running")); + // the content cant be empty + assertThat(initialValue, not(emptyOrNullString())); + return initialValue; + } else { + // async may not return results immediately, so may need an async get + assertThat(id, is(not(emptyOrNullString()))); + String isRunning = response.getHeader("X-Elasticsearch-Async-Is-Running"); + if ("?0".equals(isRunning)) { + // must have completed immediately so keep_on_completion must be true + assertThat(builder.keepOnCompletion(), is(true)); + } else { + // did not return results immediately, so we will need an async get + // Also, different format modes return different results. + switch (format) { + case "txt" -> assertThat(initialValue, emptyOrNullString()); + case "csv" -> { + assertEquals(initialValue, "\r\n"); + initialValue = ""; + } + case "tsv" -> { + assertEquals(initialValue, "\n"); + initialValue = ""; + } + } + } + // issue a second request to "async get" the results + Request getRequest = prepareAsyncGetRequest(id); + if (delimiter != null) { + getRequest.addParameter("delimiter", String.valueOf(delimiter)); + } + // If the `format` parameter is not added, the GET request will return a response + // with the `Content-Type` type due to the lack of an `Accept` header. + if (addParam) { + getRequest.addParameter("format", format); + } + // if `addParam` is false, `options` will already have an `Accept` header + getRequest.setOptions(options); + response = performRequest(getRequest); + entity = assertWarnings(response, new AssertWarnings.NoWarnings()); + } + String newValue = Streams.copyToString(new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8)); + + // assert initial contents, if any, are the same as async get contents + if (initialValue != null && initialValue.isEmpty() == false) { + assertEquals(initialValue, newValue); + } + + assertDeletable(id); + return newValue; } private static Request prepareRequest(Mode mode) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlResponseListener.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlResponseListener.java index 1c88fe6f45d8..fb7e0f651458 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlResponseListener.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlResponseListener.java @@ -22,6 +22,7 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; import org.elasticsearch.xcontent.MediaType; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.esql.arrow.ArrowFormat; import org.elasticsearch.xpack.esql.arrow.ArrowResponse; import org.elasticsearch.xpack.esql.formatter.TextFormat; @@ -87,7 +88,7 @@ public TimeValue stop() { /** * Keep the initial query for logging purposes. */ - private final String esqlQuery; + private final String esqlQueryOrId; /** * Stop the time it took to build a response to later log it. Use something thread-safe here because stopping time requires state and * {@link EsqlResponseListener} might be used from different threads. @@ -98,29 +99,23 @@ public TimeValue stop() { * To correctly time the execution of a request, a {@link EsqlResponseListener} must be constructed immediately before execution begins. */ public EsqlResponseListener(RestChannel channel, RestRequest restRequest, EsqlQueryRequest esqlRequest) { - super(channel); + this(channel, restRequest, esqlRequest.query(), EsqlMediaTypeParser.getResponseMediaType(restRequest, esqlRequest)); + } + /** + * Async query GET API does not have an EsqlQueryRequest. + */ + public EsqlResponseListener(RestChannel channel, RestRequest getRequest) { + this(channel, getRequest, getRequest.param("id"), EsqlMediaTypeParser.getResponseMediaType(getRequest, XContentType.JSON)); + } + + private EsqlResponseListener(RestChannel channel, RestRequest restRequest, String esqlQueryOrId, MediaType mediaType) { + super(channel); this.channel = channel; this.restRequest = restRequest; - this.esqlQuery = esqlRequest.query(); - mediaType = EsqlMediaTypeParser.getResponseMediaType(restRequest, esqlRequest); - - /* - * Special handling for the "delimiter" parameter which should only be - * checked for being present or not in the case of CSV format. We cannot - * override {@link BaseRestHandler#responseParams()} because this - * parameter should only be checked for CSV, not other formats. - */ - if (mediaType != CSV && restRequest.hasParam(URL_PARAM_DELIMITER)) { - String message = String.format( - Locale.ROOT, - "parameter: [%s] can only be used with the format [%s] for request [%s]", - URL_PARAM_DELIMITER, - CSV.queryParameter(), - restRequest.path() - ); - throw new IllegalArgumentException(message); - } + this.esqlQueryOrId = esqlQueryOrId; + this.mediaType = mediaType; + checkDelimiter(); } @Override @@ -197,14 +192,18 @@ public ActionListener wrapWithLogging() { listener.onResponse(r); // At this point, the StopWatch should already have been stopped, so we log a consistent time. LOGGER.debug( - "Finished execution of ESQL query.\nQuery string: [{}]\nExecution time: [{}]ms", - esqlQuery, + "Finished execution of ESQL query.\nQuery string or async ID: [{}]\nExecution time: [{}]ms", + esqlQueryOrId, getTook(r, TimeUnit.MILLISECONDS) ); }, ex -> { // In case of failure, stop the time manually before sending out the response. long timeMillis = getTook(null, TimeUnit.MILLISECONDS); - LOGGER.debug("Failed execution of ESQL query.\nQuery string: [{}]\nExecution time: [{}]ms", esqlQuery, timeMillis); + LOGGER.debug( + "Failed execution of ESQL query.\nQuery string or async ID: [{}]\nExecution time: [{}]ms", + esqlQueryOrId, + timeMillis + ); listener.onFailure(ex); }); } @@ -213,4 +212,23 @@ static void logOnFailure(Throwable throwable) { RestStatus status = ExceptionsHelper.status(throwable); LOGGER.log(status.getStatus() >= 500 ? Level.WARN : Level.DEBUG, () -> "Request failed with status [" + status + "]: ", throwable); } + + /* + * Special handling for the "delimiter" parameter which should only be + * checked for being present or not in the case of CSV format. We cannot + * override {@link BaseRestHandler#responseParams()} because this + * parameter should only be checked for CSV, not other formats. + */ + private void checkDelimiter() { + if (mediaType != CSV && restRequest.hasParam(URL_PARAM_DELIMITER)) { + String message = String.format( + Locale.ROOT, + "parameter: [%s] can only be used with the format [%s] for request [%s]", + URL_PARAM_DELIMITER, + CSV.queryParameter(), + restRequest.path() + ); + throw new IllegalArgumentException(message); + } + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/RestEsqlGetAsyncResultAction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/RestEsqlGetAsyncResultAction.java index b5a1821350e5..848a75d7fb19 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/RestEsqlGetAsyncResultAction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/RestEsqlGetAsyncResultAction.java @@ -12,7 +12,6 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; -import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; import org.elasticsearch.xpack.core.async.GetAsyncResultRequest; import java.util.List; @@ -43,7 +42,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli if (request.hasParam("keep_alive")) { get.setKeepAlive(request.paramAsTime("keep_alive", get.getKeepAlive())); } - return channel -> client.execute(EsqlAsyncGetResultAction.INSTANCE, get, new RestRefCountedChunkedToXContentListener<>(channel)); + return channel -> client.execute(EsqlAsyncGetResultAction.INSTANCE, get, new EsqlResponseListener(channel, request)); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParser.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParser.java index 17329ca2e005..1931692cea8b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParser.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParser.java @@ -42,16 +42,23 @@ public class EsqlMediaTypeParser { * combinations are detected. */ public static MediaType getResponseMediaType(RestRequest request, EsqlQueryRequest esqlRequest) { - var mediaType = request.hasParam(URL_PARAM_FORMAT) ? mediaTypeFromParams(request) : mediaTypeFromHeaders(request); + var mediaType = getResponseMediaType(request, (MediaType) null); validateColumnarRequest(esqlRequest.columnar(), mediaType); validateIncludeCCSMetadata(esqlRequest.includeCCSMetadata(), mediaType); return checkNonNullMediaType(mediaType, request); } + /* + * Retrieve the mediaType of a REST request. If no mediaType can be established from the request, return the provided default. + */ + public static MediaType getResponseMediaType(RestRequest request, MediaType defaultMediaType) { + var mediaType = request.hasParam(URL_PARAM_FORMAT) ? mediaTypeFromParams(request) : mediaTypeFromHeaders(request); + return mediaType == null ? defaultMediaType : mediaType; + } + private static MediaType mediaTypeFromHeaders(RestRequest request) { ParsedMediaType acceptType = request.getParsedAccept(); - MediaType mediaType = acceptType != null ? acceptType.toMediaType(MEDIA_TYPE_REGISTRY) : request.getXContentType(); - return checkNonNullMediaType(mediaType, request); + return acceptType != null ? acceptType.toMediaType(MEDIA_TYPE_REGISTRY) : request.getXContentType(); } private static MediaType mediaTypeFromParams(RestRequest request) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParserTests.java index 4b9166c62194..4758f83c42bb 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParserTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/EsqlMediaTypeParserTests.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.Map; +import static org.elasticsearch.xcontent.XContentType.JSON; import static org.elasticsearch.xpack.esql.formatter.TextFormat.CSV; import static org.elasticsearch.xpack.esql.formatter.TextFormat.PLAIN_TEXT; import static org.elasticsearch.xpack.esql.formatter.TextFormat.TSV; @@ -123,11 +124,17 @@ public void testIncludeCCSMetadataWithNonJSONMediaTypesInParams() { public void testNoFormat() { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> getResponseMediaType(new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).build(), createTestInstance(false)) + () -> getResponseMediaType(emptyRequest(), createTestInstance(false)) ); assertEquals(e.getMessage(), "Invalid request content type: Accept=[null], Content-Type=[null], format=[null]"); } + public void testNoContentType() { + RestRequest fakeRestRequest = emptyRequest(); + assertThat(getResponseMediaType(fakeRestRequest, CSV), is(CSV)); + assertThat(getResponseMediaType(fakeRestRequest, JSON), is(JSON)); + } + private static RestRequest reqWithAccept(String acceptHeader) { return new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withHeaders( Map.of("Content-Type", Collections.singletonList("application/json"), "Accept", Collections.singletonList(acceptHeader)) @@ -140,6 +147,10 @@ private static RestRequest reqWithParams(Map params) { ).withParams(params).build(); } + private static RestRequest emptyRequest() { + return new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).build(); + } + protected EsqlQueryRequest createTestInstance(boolean columnar) { var request = new EsqlQueryRequest(); request.columnar(columnar); From 64e0902f58a350b666708a85c9506dccdd7ecc0b Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 9 Dec 2024 12:11:55 +0000 Subject: [PATCH 11/39] Pull AWS SDK versions to top level (#118247) Today each relevant module defines the version of the AWS SDK that it uses, which means there's a risk that we use different versions in different modules. This commit pulls the version declarations to the top level to make sure we keep everything in sync. --- build-tools-internal/version.properties | 2 + modules/repository-s3/build.gradle | 12 ++---- plugins/discovery-ec2/build.gradle | 8 +--- x-pack/plugin/inference/build.gradle | 54 ++++++++++++------------- 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 29c5bc16a8c4..aaf654a37dd2 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -17,6 +17,8 @@ jna = 5.12.1 netty = 4.1.115.Final commons_lang3 = 3.9 google_oauth_client = 1.34.1 +awsv1sdk = 1.12.270 +awsv2sdk = 2.28.13 antlr4 = 4.13.1 # bouncy castle version for non-fips. fips jars use a different version diff --git a/modules/repository-s3/build.gradle b/modules/repository-s3/build.gradle index 2cfb5d23db4f..f0dc1ca71495 100644 --- a/modules/repository-s3/build.gradle +++ b/modules/repository-s3/build.gradle @@ -18,15 +18,11 @@ esplugin { classname 'org.elasticsearch.repositories.s3.S3RepositoryPlugin' } -versions << [ - 'aws': '1.12.270' -] - dependencies { - api "com.amazonaws:aws-java-sdk-s3:${versions.aws}" - api "com.amazonaws:aws-java-sdk-core:${versions.aws}" - api "com.amazonaws:aws-java-sdk-sts:${versions.aws}" - api "com.amazonaws:jmespath-java:${versions.aws}" + api "com.amazonaws:aws-java-sdk-s3:${versions.awsv1sdk}" + api "com.amazonaws:aws-java-sdk-core:${versions.awsv1sdk}" + api "com.amazonaws:aws-java-sdk-sts:${versions.awsv1sdk}" + api "com.amazonaws:jmespath-java:${versions.awsv1sdk}" api "org.apache.httpcomponents:httpclient:${versions.httpclient}" api "org.apache.httpcomponents:httpcore:${versions.httpcore}" api "commons-logging:commons-logging:${versions.commonslogging}" diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 980e2467206d..a4321a2d61f9 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -14,13 +14,9 @@ esplugin { classname 'org.elasticsearch.discovery.ec2.Ec2DiscoveryPlugin' } -versions << [ - 'aws': '1.12.270' -] - dependencies { - api "com.amazonaws:aws-java-sdk-ec2:${versions.aws}" - api "com.amazonaws:aws-java-sdk-core:${versions.aws}" + api "com.amazonaws:aws-java-sdk-ec2:${versions.awsv1sdk}" + api "com.amazonaws:aws-java-sdk-core:${versions.awsv1sdk}" api "org.apache.httpcomponents:httpclient:${versions.httpclient}" api "org.apache.httpcomponents:httpcore:${versions.httpcore}" api "commons-logging:commons-logging:${versions.commonslogging}" diff --git a/x-pack/plugin/inference/build.gradle b/x-pack/plugin/inference/build.gradle index 3c19e11a450b..1d0236a5834e 100644 --- a/x-pack/plugin/inference/build.gradle +++ b/x-pack/plugin/inference/build.gradle @@ -26,10 +26,6 @@ base { archivesName = 'x-pack-inference' } -versions << [ - 'aws2': '2.28.13' -] - dependencies { implementation project(path: ':libs:logging') compileOnly project(":server") @@ -62,36 +58,36 @@ dependencies { implementation 'io.opencensus:opencensus-contrib-http-util:0.31.1' /* AWS SDK v2 */ - implementation ("software.amazon.awssdk:bedrockruntime:${versions.aws2}") - api "software.amazon.awssdk:protocol-core:${versions.aws2}" - api "software.amazon.awssdk:aws-json-protocol:${versions.aws2}" - api "software.amazon.awssdk:third-party-jackson-core:${versions.aws2}" - api "software.amazon.awssdk:http-auth-aws:${versions.aws2}" - api "software.amazon.awssdk:checksums-spi:${versions.aws2}" - api "software.amazon.awssdk:checksums:${versions.aws2}" - api "software.amazon.awssdk:sdk-core:${versions.aws2}" + implementation ("software.amazon.awssdk:bedrockruntime:${versions.awsv2sdk}") + api "software.amazon.awssdk:protocol-core:${versions.awsv2sdk}" + api "software.amazon.awssdk:aws-json-protocol:${versions.awsv2sdk}" + api "software.amazon.awssdk:third-party-jackson-core:${versions.awsv2sdk}" + api "software.amazon.awssdk:http-auth-aws:${versions.awsv2sdk}" + api "software.amazon.awssdk:checksums-spi:${versions.awsv2sdk}" + api "software.amazon.awssdk:checksums:${versions.awsv2sdk}" + api "software.amazon.awssdk:sdk-core:${versions.awsv2sdk}" api "org.reactivestreams:reactive-streams:1.0.4" api "org.reactivestreams:reactive-streams-tck:1.0.4" - api "software.amazon.awssdk:profiles:${versions.aws2}" - api "software.amazon.awssdk:retries:${versions.aws2}" - api "software.amazon.awssdk:auth:${versions.aws2}" - api "software.amazon.awssdk:http-auth-aws-eventstream:${versions.aws2}" + api "software.amazon.awssdk:profiles:${versions.awsv2sdk}" + api "software.amazon.awssdk:retries:${versions.awsv2sdk}" + api "software.amazon.awssdk:auth:${versions.awsv2sdk}" + api "software.amazon.awssdk:http-auth-aws-eventstream:${versions.awsv2sdk}" api "software.amazon.eventstream:eventstream:1.0.1" - api "software.amazon.awssdk:http-auth-spi:${versions.aws2}" - api "software.amazon.awssdk:http-auth:${versions.aws2}" - api "software.amazon.awssdk:identity-spi:${versions.aws2}" - api "software.amazon.awssdk:http-client-spi:${versions.aws2}" - api "software.amazon.awssdk:regions:${versions.aws2}" - api "software.amazon.awssdk:annotations:${versions.aws2}" - api "software.amazon.awssdk:utils:${versions.aws2}" - api "software.amazon.awssdk:aws-core:${versions.aws2}" - api "software.amazon.awssdk:metrics-spi:${versions.aws2}" - api "software.amazon.awssdk:json-utils:${versions.aws2}" - api "software.amazon.awssdk:endpoints-spi:${versions.aws2}" - api "software.amazon.awssdk:retries-spi:${versions.aws2}" + api "software.amazon.awssdk:http-auth-spi:${versions.awsv2sdk}" + api "software.amazon.awssdk:http-auth:${versions.awsv2sdk}" + api "software.amazon.awssdk:identity-spi:${versions.awsv2sdk}" + api "software.amazon.awssdk:http-client-spi:${versions.awsv2sdk}" + api "software.amazon.awssdk:regions:${versions.awsv2sdk}" + api "software.amazon.awssdk:annotations:${versions.awsv2sdk}" + api "software.amazon.awssdk:utils:${versions.awsv2sdk}" + api "software.amazon.awssdk:aws-core:${versions.awsv2sdk}" + api "software.amazon.awssdk:metrics-spi:${versions.awsv2sdk}" + api "software.amazon.awssdk:json-utils:${versions.awsv2sdk}" + api "software.amazon.awssdk:endpoints-spi:${versions.awsv2sdk}" + api "software.amazon.awssdk:retries-spi:${versions.awsv2sdk}" /* Netty (via AWS SDKv2) */ - implementation "software.amazon.awssdk:netty-nio-client:${versions.aws2}" + implementation "software.amazon.awssdk:netty-nio-client:${versions.awsv2sdk}" runtimeOnly "io.netty:netty-buffer:${versions.netty}" runtimeOnly "io.netty:netty-codec-dns:${versions.netty}" runtimeOnly "io.netty:netty-codec-http2:${versions.netty}" From 63ee866ed6f7e27ddd3364660c5606ea20ab73e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 9 Dec 2024 14:05:48 +0100 Subject: [PATCH 12/39] ESQL: Categorize grouping function testing improvements (#118013) Added some extra tests on the CategorizeBlockHash. Added NullFold rule comments, and forced nullable() to TRUE on Categorize. --- .../esql/core/expression/Nullability.java | 17 +- .../blockhash/CategorizeBlockHashTests.java | 235 ++++++++++++++---- .../src/main/resources/categorize.csv-spec | 23 ++ .../function/grouping/Categorize.java | 7 + .../optimizer/rules/logical/FoldNull.java | 5 +- 5 files changed, 229 insertions(+), 58 deletions(-) diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Nullability.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Nullability.java index b08024a70777..d9f136a35720 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Nullability.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Nullability.java @@ -7,7 +7,18 @@ package org.elasticsearch.xpack.esql.core.expression; public enum Nullability { - TRUE, // Whether the expression can become null - FALSE, // The expression can never become null - UNKNOWN // Cannot determine if the expression supports possible null folding + /** + * Whether the expression can become null + */ + TRUE, + + /** + * The expression can never become null + */ + FALSE, + + /** + * Cannot determine if the expression supports possible null folding + */ + UNKNOWN } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java index 3c47e85a4a9c..f8428b7c3356 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java @@ -50,11 +50,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.elasticsearch.compute.operator.OperatorTestCase.runDriver; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -95,41 +95,114 @@ public void testCategorizeRaw() { page = new Page(builder.build()); } - try (BlockHash hash = new CategorizeBlockHash(blockFactory, 0, AggregatorMode.INITIAL, analysisRegistry)) { - hash.add(page, new GroupingAggregatorFunction.AddInput() { - @Override - public void add(int positionOffset, IntBlock groupIds) { - assertEquals(groupIds.getPositionCount(), positions); + try (var hash = new CategorizeBlockHash(blockFactory, 0, AggregatorMode.SINGLE, analysisRegistry)) { + for (int i = randomInt(2); i < 3; i++) { + hash.add(page, new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + assertEquals(groupIds.getPositionCount(), positions); + + assertEquals(1, groupIds.getInt(0)); + assertEquals(2, groupIds.getInt(1)); + assertEquals(2, groupIds.getInt(2)); + assertEquals(2, groupIds.getInt(3)); + assertEquals(3, groupIds.getInt(4)); + assertEquals(1, groupIds.getInt(5)); + assertEquals(1, groupIds.getInt(6)); + if (withNull) { + assertEquals(0, groupIds.getInt(7)); + } + } - assertEquals(1, groupIds.getInt(0)); - assertEquals(2, groupIds.getInt(1)); - assertEquals(2, groupIds.getInt(2)); - assertEquals(2, groupIds.getInt(3)); - assertEquals(3, groupIds.getInt(4)); - assertEquals(1, groupIds.getInt(5)); - assertEquals(1, groupIds.getInt(6)); - if (withNull) { - assertEquals(0, groupIds.getInt(7)); + @Override + public void add(int positionOffset, IntVector groupIds) { + add(positionOffset, groupIds.asBlock()); } - } - @Override - public void add(int positionOffset, IntVector groupIds) { - add(positionOffset, groupIds.asBlock()); - } + @Override + public void close() { + fail("hashes should not close AddInput"); + } + }); - @Override - public void close() { - fail("hashes should not close AddInput"); - } - }); + assertHashState(hash, withNull, ".*?Connected.+?to.*?", ".*?Connection.+?error.*?", ".*?Disconnected.*?"); + } } finally { page.releaseBlocks(); } - // TODO: randomize and try multiple pages. - // TODO: assert the state of the BlockHash after adding pages. Including the categorizer state. - // TODO: also test the lookup method and other stuff. + // TODO: randomize values? May give wrong results + // TODO: assert the categorizer state after adding pages. + } + + public void testCategorizeRawMultivalue() { + final Page page; + boolean withNull = randomBoolean(); + final int positions = 3 + (withNull ? 1 : 0); + try (BytesRefBlock.Builder builder = blockFactory.newBytesRefBlockBuilder(positions)) { + builder.beginPositionEntry(); + builder.appendBytesRef(new BytesRef("Connected to 10.1.0.1")); + builder.appendBytesRef(new BytesRef("Connection error")); + builder.appendBytesRef(new BytesRef("Connection error")); + builder.appendBytesRef(new BytesRef("Connection error")); + builder.endPositionEntry(); + builder.appendBytesRef(new BytesRef("Disconnected")); + builder.beginPositionEntry(); + builder.appendBytesRef(new BytesRef("Connected to 10.1.0.2")); + builder.appendBytesRef(new BytesRef("Connected to 10.1.0.3")); + builder.endPositionEntry(); + if (withNull) { + if (randomBoolean()) { + builder.appendNull(); + } else { + builder.appendBytesRef(new BytesRef("")); + } + } + page = new Page(builder.build()); + } + + try (var hash = new CategorizeBlockHash(blockFactory, 0, AggregatorMode.SINGLE, analysisRegistry)) { + for (int i = randomInt(2); i < 3; i++) { + hash.add(page, new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + assertEquals(groupIds.getPositionCount(), positions); + + assertThat(groupIds.getFirstValueIndex(0), equalTo(0)); + assertThat(groupIds.getValueCount(0), equalTo(4)); + assertThat(groupIds.getFirstValueIndex(1), equalTo(4)); + assertThat(groupIds.getValueCount(1), equalTo(1)); + assertThat(groupIds.getFirstValueIndex(2), equalTo(5)); + assertThat(groupIds.getValueCount(2), equalTo(2)); + + assertEquals(1, groupIds.getInt(0)); + assertEquals(2, groupIds.getInt(1)); + assertEquals(2, groupIds.getInt(2)); + assertEquals(2, groupIds.getInt(3)); + assertEquals(3, groupIds.getInt(4)); + assertEquals(1, groupIds.getInt(5)); + assertEquals(1, groupIds.getInt(6)); + if (withNull) { + assertEquals(0, groupIds.getInt(7)); + } + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + add(positionOffset, groupIds.asBlock()); + } + + @Override + public void close() { + fail("hashes should not close AddInput"); + } + }); + + assertHashState(hash, withNull, ".*?Connected.+?to.*?", ".*?Connection.+?error.*?", ".*?Disconnected.*?"); + } + } finally { + page.releaseBlocks(); + } } public void testCategorizeIntermediate() { @@ -226,18 +299,18 @@ public void close() { page2.releaseBlocks(); } - try (BlockHash intermediateHash = new CategorizeBlockHash(blockFactory, 0, AggregatorMode.INTERMEDIATE, null)) { + try (var intermediateHash = new CategorizeBlockHash(blockFactory, 0, AggregatorMode.FINAL, null)) { intermediateHash.add(intermediatePage1, new GroupingAggregatorFunction.AddInput() { @Override public void add(int positionOffset, IntBlock groupIds) { - Set values = IntStream.range(0, groupIds.getPositionCount()) + List values = IntStream.range(0, groupIds.getPositionCount()) .map(groupIds::getInt) .boxed() - .collect(Collectors.toSet()); + .collect(Collectors.toList()); if (withNull) { - assertEquals(Set.of(0, 1, 2), values); + assertEquals(List.of(0, 1, 2), values); } else { - assertEquals(Set.of(1, 2), values); + assertEquals(List.of(1, 2), values); } } @@ -252,28 +325,39 @@ public void close() { } }); - intermediateHash.add(intermediatePage2, new GroupingAggregatorFunction.AddInput() { - @Override - public void add(int positionOffset, IntBlock groupIds) { - Set values = IntStream.range(0, groupIds.getPositionCount()) - .map(groupIds::getInt) - .boxed() - .collect(Collectors.toSet()); - // The category IDs {0, 1, 2} should map to groups {0, 2, 3}, because - // 0 matches an existing category (Connected to ...), and the others are new. - assertEquals(Set.of(1, 3, 4), values); - } + for (int i = randomInt(2); i < 3; i++) { + intermediateHash.add(intermediatePage2, new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + List values = IntStream.range(0, groupIds.getPositionCount()) + .map(groupIds::getInt) + .boxed() + .collect(Collectors.toList()); + // The category IDs {1, 2, 3} should map to groups {1, 3, 4}, because + // 1 matches an existing category (Connected to ...), and the others are new. + assertEquals(List.of(3, 1, 4), values); + } - @Override - public void add(int positionOffset, IntVector groupIds) { - add(positionOffset, groupIds.asBlock()); - } + @Override + public void add(int positionOffset, IntVector groupIds) { + add(positionOffset, groupIds.asBlock()); + } - @Override - public void close() { - fail("hashes should not close AddInput"); - } - }); + @Override + public void close() { + fail("hashes should not close AddInput"); + } + }); + + assertHashState( + intermediateHash, + withNull, + ".*?Connected.+?to.*?", + ".*?Connection.+?error.*?", + ".*?Disconnected.*?", + ".*?System.+?shutdown.*?" + ); + } } finally { intermediatePage1.releaseBlocks(); intermediatePage2.releaseBlocks(); @@ -457,4 +541,49 @@ public void testCategorize_withDriver() { private BlockHash.GroupSpec makeGroupSpec() { return new BlockHash.GroupSpec(0, ElementType.BYTES_REF, true); } + + private void assertHashState(CategorizeBlockHash hash, boolean withNull, String... expectedKeys) { + // Check the keys + Block[] blocks = null; + try { + blocks = hash.getKeys(); + assertThat(blocks, arrayWithSize(1)); + + var keysBlock = (BytesRefBlock) blocks[0]; + assertThat(keysBlock.getPositionCount(), equalTo(expectedKeys.length + (withNull ? 1 : 0))); + + if (withNull) { + assertTrue(keysBlock.isNull(0)); + } + + for (int i = 0; i < expectedKeys.length; i++) { + int position = i + (withNull ? 1 : 0); + String key = keysBlock.getBytesRef(position, new BytesRef()).utf8ToString(); + assertThat(key, equalTo(expectedKeys[i])); + } + } finally { + if (blocks != null) { + Releasables.close(blocks); + } + } + + // Check the nonEmpty() result + try (IntVector nonEmptyKeys = hash.nonEmpty()) { + int oneIfNull = withNull ? 1 : 0; + assertThat(nonEmptyKeys.getPositionCount(), equalTo(expectedKeys.length + oneIfNull)); + + for (int i = 0; i < expectedKeys.length + oneIfNull; i++) { + assertThat(nonEmptyKeys.getInt(i), equalTo(i + 1 - oneIfNull)); + } + } + + // Check seenGroupIds() + try (var seenGroupIds = hash.seenGroupIds(blockFactory.bigArrays())) { + assertThat(seenGroupIds.get(0), equalTo(withNull)); + + for (int i = 1; i <= expectedKeys.length; i++) { + assertThat(seenGroupIds.get(i), equalTo(true)); + } + } + } } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec index 804c1c56a1eb..4ce43961a707 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec @@ -374,6 +374,29 @@ COUNT():long | category:keyword 7 | null ; +on const null +required_capability: categorize_v5 + +FROM sample_data + | STATS COUNT(), SUM(event_duration) BY category=CATEGORIZE(null) + | SORT category +; + +COUNT():long | SUM(event_duration):long | category:keyword + 7 | 23231327 | null +; + +on null row +required_capability: categorize_v5 + +ROW message = null, str = ["a", "b", "c"] +| STATS COUNT(), VALUES(str) BY category=CATEGORIZE(message) +; + +COUNT():long | VALUES(str):keyword | category:keyword + 1 | [a, b, c] | null +; + filtering out all data required_capability: categorize_v5 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java index e2c04ecb15b5..ded913a78bdf 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java @@ -13,6 +13,7 @@ import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator; import org.elasticsearch.xpack.esql.capabilities.Validatable; import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.Nullability; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -92,6 +93,12 @@ public boolean foldable() { return false; } + @Override + public Nullability nullable() { + // Both nulls and empty strings result in null values + return Nullability.TRUE; + } + @Override public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { throw new UnsupportedOperationException("CATEGORIZE is only evaluated during aggregations"); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java index 4f97bf60bd86..747864625e65 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNull.java @@ -41,8 +41,9 @@ public Expression rule(Expression e) { if (Expressions.isGuaranteedNull(in.value())) { return Literal.of(in, null); } - } else if (e instanceof Alias == false - && e.nullable() == Nullability.TRUE + } else if (e instanceof Alias == false && e.nullable() == Nullability.TRUE + // Categorize function stays as a STATS grouping (It isn't moved to an early EVAL like other groupings), + // so folding it to null would currently break the plan, as we don't create an attribute/channel for that null value. && e instanceof Categorize == false && Expressions.anyMatch(e.children(), Expressions::isGuaranteedNull)) { return Literal.of(e, null); From ccc416ddf9e92a09865fdd765ec5eefb6d9456b2 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Mon, 9 Dec 2024 14:13:16 +0100 Subject: [PATCH 13/39] Ignore order in LookupMessageFromIndexKeepReordered (#118256) Fix #118150 Fix #118151 We should also ignore the order for this test, as the output rows are not deterministic in all cases. --- muted-tests.yml | 6 ------ .../qa/testFixtures/src/main/resources/lookup-join.csv-spec | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index dcaa415a6796..eecb7ac3d7e5 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -245,12 +245,6 @@ tests: - class: org.elasticsearch.packaging.test.ArchiveTests method: test41AutoconfigurationNotTriggeredWhenNodeCannotContainData issue: https://github.com/elastic/elasticsearch/issues/118110 -- class: org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT - method: test {lookup-join.LookupMessageFromIndexKeepReordered SYNC} - issue: https://github.com/elastic/elasticsearch/issues/118150 -- class: org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT - method: test {lookup-join.LookupMessageFromIndexKeepReordered ASYNC} - issue: https://github.com/elastic/elasticsearch/issues/118151 - class: org.elasticsearch.xpack.remotecluster.CrossClusterEsqlRCS2UnavailableRemotesIT method: testEsqlRcs2UnavailableRemoteScenarios issue: https://github.com/elastic/elasticsearch/issues/117419 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index 2d4c105cfff2..b01e12fa4f47 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -283,6 +283,7 @@ FROM sample_data | LOOKUP JOIN message_types_lookup ON message | KEEP type, client_ip, event_duration, message ; +ignoreOrder:true type:keyword | client_ip:ip | event_duration:long | message:keyword Success | 172.21.3.15 | 1756467 | Connected to 10.1.0.1 From 931f675891a78cdf907e411cbfb95033a55f7c36 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:25:10 +0100 Subject: [PATCH 14/39] Update connectors overview diagram (#118261) --- .../docs/images/connectors-overview.png | Bin 315897 -> 0 bytes .../docs/images/connectors-overview.svg | 70 ++++++++++++++++++ docs/reference/connector/docs/index.asciidoc | 2 +- 3 files changed, 71 insertions(+), 1 deletion(-) delete mode 100644 docs/reference/connector/docs/images/connectors-overview.png create mode 100644 docs/reference/connector/docs/images/connectors-overview.svg diff --git a/docs/reference/connector/docs/images/connectors-overview.png b/docs/reference/connector/docs/images/connectors-overview.png deleted file mode 100644 index 4d0edfeb6adaeb9e3dbaa4d18432a3dd952f531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 315897 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 z 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≧#mm(*7-2Pr&yx?ZPT(UU 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

a?#d&=x4_fIsI=Q=mFO%b@0K=)uhNeX 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�~erdnl##I@6 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%0A14TPkHmth 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�mRwCV;@rxg&0 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

g^MhyNf$c7tK$u&va`g|0^l< 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 diff --git a/docs/reference/connector/docs/images/connectors-overview.svg b/docs/reference/connector/docs/images/connectors-overview.svg new file mode 100644 index 000000000000..0a7fb30c61d6 --- /dev/null +++ b/docs/reference/connector/docs/images/connectors-overview.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/reference/connector/docs/index.asciidoc b/docs/reference/connector/docs/index.asciidoc index 481e124a1a11..dfca45f86ebc 100644 --- a/docs/reference/connector/docs/index.asciidoc +++ b/docs/reference/connector/docs/index.asciidoc @@ -72,7 +72,7 @@ Refer to <> for details. The following diagram provides a high-level overview of the Elastic connectors offering and some key facts. -image::connectors-overview.png[align="center",width="100%"] +image::connectors-overview.svg[align="center",width="100%"] [discrete#es-connectors-overview-available-connectors] == Available connectors and feature support From bc25a73543d1f7b0e523e96dd81786147d2dd0b3 Mon Sep 17 00:00:00 2001 From: Pete Gillin Date: Mon, 9 Dec 2024 14:28:24 +0000 Subject: [PATCH 15/39] Update `UpdateForV9` in `AttachmentProcessor` (#118186) We are not going to make this change in V9. We may do it in V10. This change just bumps the annotation to remind us to revisit. Since we are living with this for a while, it seems worth improving the documentation. This now encourages explicitly setting the option one way or the other, since you get a warning if you omit it. It also changes the existing examples to use true rather than false, as that's our recommendation. And it adds a new section with an example where it's true, and moves the content previously in a note into that section. --- .../ingest/processors/attachment.asciidoc | 91 ++++++++++++++----- .../attachment/AttachmentProcessor.java | 10 +- 2 files changed, 73 insertions(+), 28 deletions(-) diff --git a/docs/reference/ingest/processors/attachment.asciidoc b/docs/reference/ingest/processors/attachment.asciidoc index fd2866906c1d..bd5b8db562ae 100644 --- a/docs/reference/ingest/processors/attachment.asciidoc +++ b/docs/reference/ingest/processors/attachment.asciidoc @@ -19,15 +19,15 @@ representation. The processor will skip the base64 decoding then. .Attachment options [options="header"] |====== -| Name | Required | Default | Description -| `field` | yes | - | The field to get the base64 encoded field from -| `target_field` | no | attachment | The field that will hold the attachment information -| `indexed_chars` | no | 100000 | The number of chars being used for extraction to prevent huge fields. Use `-1` for no limit. -| `indexed_chars_field` | no | `null` | Field name from which you can overwrite the number of chars being used for extraction. See `indexed_chars`. -| `properties` | no | all properties | Array of properties to select to be stored. Can be `content`, `title`, `name`, `author`, `keywords`, `date`, `content_type`, `content_length`, `language` -| `ignore_missing` | no | `false` | If `true` and `field` does not exist, the processor quietly exits without modifying the document -| `remove_binary` | no | `false` | If `true`, the binary `field` will be removed from the document -| `resource_name` | no | | Field containing the name of the resource to decode. If specified, the processor passes this resource name to the underlying Tika library to enable https://tika.apache.org/1.24.1/detection.html#Resource_Name_Based_Detection[Resource Name Based Detection]. +| Name | Required | Default | Description +| `field` | yes | - | The field to get the base64 encoded field from +| `target_field` | no | attachment | The field that will hold the attachment information +| `indexed_chars` | no | 100000 | The number of chars being used for extraction to prevent huge fields. Use `-1` for no limit. +| `indexed_chars_field` | no | `null` | Field name from which you can overwrite the number of chars being used for extraction. See `indexed_chars`. +| `properties` | no | all properties | Array of properties to select to be stored. Can be `content`, `title`, `name`, `author`, `keywords`, `date`, `content_type`, `content_length`, `language` +| `ignore_missing` | no | `false` | If `true` and `field` does not exist, the processor quietly exits without modifying the document +| `remove_binary` | encouraged | `false` | If `true`, the binary `field` will be removed from the document. This option is not required, but setting it explicitly is encouraged, and omitting it will result in a warning. +| `resource_name` | no | | Field containing the name of the resource to decode. If specified, the processor passes this resource name to the underlying Tika library to enable https://tika.apache.org/1.24.1/detection.html#Resource_Name_Based_Detection[Resource Name Based Detection]. |====== [discrete] @@ -58,7 +58,7 @@ PUT _ingest/pipeline/attachment { "attachment" : { "field" : "data", - "remove_binary": false + "remove_binary": true } } ] @@ -82,7 +82,6 @@ The document's `attachment` object contains extracted properties for the file: "_seq_no": 22, "_primary_term": 1, "_source": { - "data": "e1xydGYxXGFuc2kNCkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBhbWV0DQpccGFyIH0=", "attachment": { "content_type": "application/rtf", "language": "ro", @@ -94,9 +93,6 @@ The document's `attachment` object contains extracted properties for the file: ---- // TESTRESPONSE[s/"_seq_no": \d+/"_seq_no" : $body._seq_no/ s/"_primary_term" : 1/"_primary_term" : $body._primary_term/] -NOTE: Keeping the binary as a field within the document might consume a lot of resources. It is highly recommended - to remove that field from the document. Set `remove_binary` to `true` to automatically remove the field. - [[attachment-fields]] ==== Exported fields @@ -143,7 +139,7 @@ PUT _ingest/pipeline/attachment "attachment" : { "field" : "data", "properties": [ "content", "title" ], - "remove_binary": false + "remove_binary": true } } ] @@ -154,6 +150,59 @@ NOTE: Extracting contents from binary data is a resource intensive operation and consumes a lot of resources. It is highly recommended to run pipelines using this processor in a dedicated ingest node. +[[attachment-keep-binary]] +==== Keeping the attachment binary + +Keeping the binary as a field within the document might consume a lot of resources. It is highly recommended to remove +that field from the document, by setting `remove_binary` to `true` to automatically remove the field, as in the other +examples shown on this page. If you _do_ want to keep the binary field, explicitly set `remove_binary` to `false` to +avoid the warning you get from omitting it: + +[source,console] +---- +PUT _ingest/pipeline/attachment +{ + "description" : "Extract attachment information including original binary", + "processors" : [ + { + "attachment" : { + "field" : "data", + "remove_binary": false + } + } + ] +} +PUT my-index-000001/_doc/my_id?pipeline=attachment +{ + "data": "e1xydGYxXGFuc2kNCkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBhbWV0DQpccGFyIH0=" +} +GET my-index-000001/_doc/my_id +---- + +The document's `_source` object includes the original binary field: + +[source,console-result] +---- +{ + "found": true, + "_index": "my-index-000001", + "_id": "my_id", + "_version": 1, + "_seq_no": 22, + "_primary_term": 1, + "_source": { + "data": "e1xydGYxXGFuc2kNCkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBhbWV0DQpccGFyIH0=", + "attachment": { + "content_type": "application/rtf", + "language": "ro", + "content": "Lorem ipsum dolor sit amet", + "content_length": 28 + } + } +} +---- +// TESTRESPONSE[s/"_seq_no": \d+/"_seq_no" : $body._seq_no/ s/"_primary_term" : 1/"_primary_term" : $body._primary_term/] + [[attachment-cbor]] ==== Use the attachment processor with CBOR @@ -170,7 +219,7 @@ PUT _ingest/pipeline/cbor-attachment { "attachment" : { "field" : "data", - "remove_binary": false + "remove_binary": true } } ] @@ -226,7 +275,7 @@ PUT _ingest/pipeline/attachment "field" : "data", "indexed_chars" : 11, "indexed_chars_field" : "max_size", - "remove_binary": false + "remove_binary": true } } ] @@ -250,7 +299,6 @@ Returns this: "_seq_no": 35, "_primary_term": 1, "_source": { - "data": "e1xydGYxXGFuc2kNCkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBhbWV0DQpccGFyIH0=", "attachment": { "content_type": "application/rtf", "language": "is", @@ -274,7 +322,7 @@ PUT _ingest/pipeline/attachment "field" : "data", "indexed_chars" : 11, "indexed_chars_field" : "max_size", - "remove_binary": false + "remove_binary": true } } ] @@ -299,7 +347,6 @@ Returns this: "_seq_no": 40, "_primary_term": 1, "_source": { - "data": "e1xydGYxXGFuc2kNCkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBhbWV0DQpccGFyIH0=", "max_size": 5, "attachment": { "content_type": "application/rtf", @@ -358,7 +405,7 @@ PUT _ingest/pipeline/attachment "attachment": { "target_field": "_ingest._value.attachment", "field": "_ingest._value.data", - "remove_binary": false + "remove_binary": true } } } @@ -396,7 +443,6 @@ Returns this: "attachments" : [ { "filename" : "ipsum.txt", - "data" : "dGhpcyBpcwpqdXN0IHNvbWUgdGV4dAo=", "attachment" : { "content_type" : "text/plain; charset=ISO-8859-1", "language" : "en", @@ -406,7 +452,6 @@ Returns this: }, { "filename" : "test.txt", - "data" : "VGhpcyBpcyBhIHRlc3QK", "attachment" : { "content_type" : "text/plain; charset=ISO-8859-1", "language" : "en", diff --git a/modules/ingest-attachment/src/main/java/org/elasticsearch/ingest/attachment/AttachmentProcessor.java b/modules/ingest-attachment/src/main/java/org/elasticsearch/ingest/attachment/AttachmentProcessor.java index 007fe39d72e6..83a7bdf7e224 100644 --- a/modules/ingest-attachment/src/main/java/org/elasticsearch/ingest/attachment/AttachmentProcessor.java +++ b/modules/ingest-attachment/src/main/java/org/elasticsearch/ingest/attachment/AttachmentProcessor.java @@ -18,7 +18,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; -import org.elasticsearch.core.UpdateForV9; +import org.elasticsearch.core.UpdateForV10; import org.elasticsearch.ingest.AbstractProcessor; import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.ingest.Processor; @@ -196,7 +196,7 @@ public IngestDocument execute(IngestDocument ingestDocument) { * @param property property to add * @param value value to add */ - private void addAdditionalField(Map additionalFields, Property property, String value) { + private void addAdditionalField(Map additionalFields, Property property, String value) { if (properties.contains(property) && Strings.hasLength(value)) { additionalFields.put(property.toLowerCase(), value); } @@ -233,7 +233,7 @@ public AttachmentProcessor create( String processorTag, String description, Map config - ) throws Exception { + ) { String field = readStringProperty(TYPE, processorTag, config, "field"); String resourceName = readOptionalStringProperty(TYPE, processorTag, config, "resource_name"); String targetField = readStringProperty(TYPE, processorTag, config, "target_field", "attachment"); @@ -241,8 +241,8 @@ public AttachmentProcessor create( int indexedChars = readIntProperty(TYPE, processorTag, config, "indexed_chars", NUMBER_OF_CHARS_INDEXED); boolean ignoreMissing = readBooleanProperty(TYPE, processorTag, config, "ignore_missing", false); String indexedCharsField = readOptionalStringProperty(TYPE, processorTag, config, "indexed_chars_field"); - @UpdateForV9(owner = UpdateForV9.Owner.DATA_MANAGEMENT) - // update the [remove_binary] default to be 'true' assuming enough time has passed. Deprecated in September 2022. + @UpdateForV10(owner = UpdateForV10.Owner.DATA_MANAGEMENT) + // Revisit whether we want to update the [remove_binary] default to be 'true' - would need to find a way to do this safely Boolean removeBinary = readOptionalBooleanProperty(TYPE, processorTag, config, "remove_binary"); if (removeBinary == null) { DEPRECATION_LOGGER.warn( From 527c3e3041d7dc3098799677878a5887809fcbdd Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:34:07 +1100 Subject: [PATCH 16/39] Mute org.elasticsearch.xpack.test.rest.XPackRestIT test {p0=migrate/10_reindex/Test Reindex With Bad Data Stream Name} #118272 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index eecb7ac3d7e5..edb48e0cc3a7 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -288,6 +288,9 @@ tests: - class: org.elasticsearch.packaging.test.ArchiveTests method: test60StartAndStop issue: https://github.com/elastic/elasticsearch/issues/118216 +- class: org.elasticsearch.xpack.test.rest.XPackRestIT + method: test {p0=migrate/10_reindex/Test Reindex With Bad Data Stream Name} + issue: https://github.com/elastic/elasticsearch/issues/118272 # Examples: # From e9f507464c729fbcb8fa0fc57948c163bee1a125 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:34:24 +1100 Subject: [PATCH 17/39] Mute org.elasticsearch.xpack.test.rest.XPackRestIT test {p0=migrate/10_reindex/Test Reindex With Unsupported Mode} #118273 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index edb48e0cc3a7..ecb3c0293d63 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -291,6 +291,9 @@ tests: - class: org.elasticsearch.xpack.test.rest.XPackRestIT method: test {p0=migrate/10_reindex/Test Reindex With Bad Data Stream Name} issue: https://github.com/elastic/elasticsearch/issues/118272 +- class: org.elasticsearch.xpack.test.rest.XPackRestIT + method: test {p0=migrate/10_reindex/Test Reindex With Unsupported Mode} + issue: https://github.com/elastic/elasticsearch/issues/118273 # Examples: # From e593eb377f98975f95eef4fa6a1c1caae1238d48 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:34:41 +1100 Subject: [PATCH 18/39] Mute org.elasticsearch.xpack.test.rest.XPackRestIT test {p0=migrate/10_reindex/Test Reindex With Nonexistent Data Stream} #118274 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index ecb3c0293d63..f922a1a27b62 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -294,6 +294,9 @@ tests: - class: org.elasticsearch.xpack.test.rest.XPackRestIT method: test {p0=migrate/10_reindex/Test Reindex With Unsupported Mode} issue: https://github.com/elastic/elasticsearch/issues/118273 +- class: org.elasticsearch.xpack.test.rest.XPackRestIT + method: test {p0=migrate/10_reindex/Test Reindex With Nonexistent Data Stream} + issue: https://github.com/elastic/elasticsearch/issues/118274 # Examples: # From f40dc99f9101e97f4100df2a63e73757be52ffa5 Mon Sep 17 00:00:00 2001 From: Dan Rubinstein Date: Mon, 9 Dec 2024 10:14:32 -0500 Subject: [PATCH 19/39] Adding transforms migration guide for 9.0 (#117353) * Adding transforms migration guide for 9.0 * Adding shared transform attribute and simplifying wording --------- Co-authored-by: Elastic Machine --- .../migrate_9_0/transforms-migration-guide.asciidoc | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/reference/migration/migrate_9_0/transforms-migration-guide.asciidoc diff --git a/docs/reference/migration/migrate_9_0/transforms-migration-guide.asciidoc b/docs/reference/migration/migrate_9_0/transforms-migration-guide.asciidoc new file mode 100644 index 000000000000..d41c524d68d5 --- /dev/null +++ b/docs/reference/migration/migrate_9_0/transforms-migration-guide.asciidoc @@ -0,0 +1,9 @@ +[[transforms-migration-guide]] +== {transforms-cap} migration guide +This migration guide helps you upgrade your {transforms} to work with the 9.0 release. Each section outlines a breaking change and any manual steps needed to upgrade your {transforms} to be compatible with 9.0. + + +=== Updating deprecated {transform} roles (`data_frame_transforms_admin` and `data_frame_transforms_user`) +If you have existing {transforms} that use deprecated {transform} roles (`data_frame_transforms_admin` or `data_frame_transforms_user`) you must update them to use the new equivalent {transform} roles (`transform_admin` or `transform_user`). To update your {transform} roles: +1. Switch to a user with the `transform_admin` role (to replace `data_frame_transforms_admin`) or the `transform_user` role (to replace `data_frame_transforms_user`). +2. Call the <> with that user. From 2ecf981f24ca5d480466a4fcc669aeb52d063657 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 9 Dec 2024 15:22:48 +0000 Subject: [PATCH 20/39] [ML] Refactor the Chunker classes to return offsets (#117977) --- .../xpack/inference/chunking/Chunker.java | 4 +- .../chunking/EmbeddingRequestChunker.java | 55 ++++++++++++------- .../chunking/SentenceBoundaryChunker.java | 20 ++++--- .../chunking/WordBoundaryChunker.java | 22 +++----- .../EmbeddingRequestChunkerTests.java | 24 ++++---- .../SentenceBoundaryChunkerTests.java | 34 +++++++++--- .../chunking/WordBoundaryChunkerTests.java | 36 ++++++++---- 7 files changed, 119 insertions(+), 76 deletions(-) diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/Chunker.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/Chunker.java index af7c706c807e..b8908ee139c2 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/Chunker.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/Chunker.java @@ -12,5 +12,7 @@ import java.util.List; public interface Chunker { - List chunk(String input, ChunkingSettings chunkingSettings); + record ChunkOffset(int start, int end) {}; + + List chunk(String input, ChunkingSettings chunkingSettings); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunker.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunker.java index c5897f32d6eb..2aef54e56f4b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunker.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunker.java @@ -68,7 +68,7 @@ public static EmbeddingType fromDenseVectorElementType(DenseVectorFieldMapper.El private final EmbeddingType embeddingType; private final ChunkingSettings chunkingSettings; - private List> chunkedInputs; + private List chunkedOffsets; private List>> floatResults; private List>> byteResults; private List>> sparseResults; @@ -109,7 +109,7 @@ public EmbeddingRequestChunker( } private void splitIntoBatchedRequests(List inputs) { - Function> chunkFunction; + Function> chunkFunction; if (chunkingSettings != null) { var chunker = ChunkerBuilder.fromChunkingStrategy(chunkingSettings.getChunkingStrategy()); chunkFunction = input -> chunker.chunk(input, chunkingSettings); @@ -118,7 +118,7 @@ private void splitIntoBatchedRequests(List inputs) { chunkFunction = input -> chunker.chunk(input, wordsPerChunk, chunkOverlap); } - chunkedInputs = new ArrayList<>(inputs.size()); + chunkedOffsets = new ArrayList<>(inputs.size()); switch (embeddingType) { case FLOAT -> floatResults = new ArrayList<>(inputs.size()); case BYTE -> byteResults = new ArrayList<>(inputs.size()); @@ -128,18 +128,19 @@ private void splitIntoBatchedRequests(List inputs) { for (int i = 0; i < inputs.size(); i++) { var chunks = chunkFunction.apply(inputs.get(i)); - int numberOfSubBatches = addToBatches(chunks, i); + var offSetsAndInput = new ChunkOffsetsAndInput(chunks, inputs.get(i)); + int numberOfSubBatches = addToBatches(offSetsAndInput, i); // size the results array with the expected number of request/responses switch (embeddingType) { case FLOAT -> floatResults.add(new AtomicArray<>(numberOfSubBatches)); case BYTE -> byteResults.add(new AtomicArray<>(numberOfSubBatches)); case SPARSE -> sparseResults.add(new AtomicArray<>(numberOfSubBatches)); } - chunkedInputs.add(chunks); + chunkedOffsets.add(offSetsAndInput); } } - private int addToBatches(List chunks, int inputIndex) { + private int addToBatches(ChunkOffsetsAndInput chunk, int inputIndex) { BatchRequest lastBatch; if (batchedRequests.isEmpty()) { lastBatch = new BatchRequest(new ArrayList<>()); @@ -157,16 +158,24 @@ private int addToBatches(List chunks, int inputIndex) { if (freeSpace > 0) { // use any free space in the previous batch before creating new batches - int toAdd = Math.min(freeSpace, chunks.size()); - lastBatch.addSubBatch(new SubBatch(chunks.subList(0, toAdd), new SubBatchPositionsAndCount(inputIndex, chunkIndex++, toAdd))); + int toAdd = Math.min(freeSpace, chunk.offsets().size()); + lastBatch.addSubBatch( + new SubBatch( + new ChunkOffsetsAndInput(chunk.offsets().subList(0, toAdd), chunk.input()), + new SubBatchPositionsAndCount(inputIndex, chunkIndex++, toAdd) + ) + ); } int start = freeSpace; - while (start < chunks.size()) { - int toAdd = Math.min(maxNumberOfInputsPerBatch, chunks.size() - start); + while (start < chunk.offsets().size()) { + int toAdd = Math.min(maxNumberOfInputsPerBatch, chunk.offsets().size() - start); var batch = new BatchRequest(new ArrayList<>()); batch.addSubBatch( - new SubBatch(chunks.subList(start, start + toAdd), new SubBatchPositionsAndCount(inputIndex, chunkIndex++, toAdd)) + new SubBatch( + new ChunkOffsetsAndInput(chunk.offsets().subList(start, start + toAdd), chunk.input()), + new SubBatchPositionsAndCount(inputIndex, chunkIndex++, toAdd) + ) ); batchedRequests.add(batch); start += toAdd; @@ -333,8 +342,8 @@ public void onFailure(Exception e) { } private void sendResponse() { - var response = new ArrayList(chunkedInputs.size()); - for (int i = 0; i < chunkedInputs.size(); i++) { + var response = new ArrayList(chunkedOffsets.size()); + for (int i = 0; i < chunkedOffsets.size(); i++) { if (errors.get(i) != null) { response.add(errors.get(i)); } else { @@ -348,9 +357,9 @@ private void sendResponse() { private ChunkedInferenceServiceResults mergeResultsWithInputs(int resultIndex) { return switch (embeddingType) { - case FLOAT -> mergeFloatResultsWithInputs(chunkedInputs.get(resultIndex), floatResults.get(resultIndex)); - case BYTE -> mergeByteResultsWithInputs(chunkedInputs.get(resultIndex), byteResults.get(resultIndex)); - case SPARSE -> mergeSparseResultsWithInputs(chunkedInputs.get(resultIndex), sparseResults.get(resultIndex)); + case FLOAT -> mergeFloatResultsWithInputs(chunkedOffsets.get(resultIndex).toChunkText(), floatResults.get(resultIndex)); + case BYTE -> mergeByteResultsWithInputs(chunkedOffsets.get(resultIndex).toChunkText(), byteResults.get(resultIndex)); + case SPARSE -> mergeSparseResultsWithInputs(chunkedOffsets.get(resultIndex).toChunkText(), sparseResults.get(resultIndex)); }; } @@ -428,7 +437,7 @@ public void addSubBatch(SubBatch sb) { } public List inputs() { - return subBatches.stream().flatMap(s -> s.requests().stream()).collect(Collectors.toList()); + return subBatches.stream().flatMap(s -> s.requests().toChunkText().stream()).collect(Collectors.toList()); } } @@ -441,9 +450,15 @@ public record BatchRequestAndListener(BatchRequest batch, ActionListener requests, SubBatchPositionsAndCount positions) { - public int size() { - return requests.size(); + record SubBatch(ChunkOffsetsAndInput requests, SubBatchPositionsAndCount positions) { + int size() { + return requests.offsets().size(); + } + } + + record ChunkOffsetsAndInput(List offsets, String input) { + List toChunkText() { + return offsets.stream().map(o -> input.substring(o.start(), o.end())).collect(Collectors.toList()); } } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java index 5df940d6a3fb..b2d6c83b8921 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java @@ -34,7 +34,6 @@ public class SentenceBoundaryChunker implements Chunker { public SentenceBoundaryChunker() { sentenceIterator = BreakIterator.getSentenceInstance(Locale.ROOT); wordIterator = BreakIterator.getWordInstance(Locale.ROOT); - } /** @@ -45,7 +44,7 @@ public SentenceBoundaryChunker() { * @return The input text chunked */ @Override - public List chunk(String input, ChunkingSettings chunkingSettings) { + public List chunk(String input, ChunkingSettings chunkingSettings) { if (chunkingSettings instanceof SentenceBoundaryChunkingSettings sentenceBoundaryChunkingSettings) { return chunk(input, sentenceBoundaryChunkingSettings.maxChunkSize, sentenceBoundaryChunkingSettings.sentenceOverlap > 0); } else { @@ -65,8 +64,8 @@ public List chunk(String input, ChunkingSettings chunkingSettings) { * @param maxNumberWordsPerChunk Maximum size of the chunk * @return The input text chunked */ - public List chunk(String input, int maxNumberWordsPerChunk, boolean includePrecedingSentence) { - var chunks = new ArrayList(); + public List chunk(String input, int maxNumberWordsPerChunk, boolean includePrecedingSentence) { + var chunks = new ArrayList(); sentenceIterator.setText(input); wordIterator.setText(input); @@ -91,7 +90,7 @@ public List chunk(String input, int maxNumberWordsPerChunk, boolean incl int nextChunkWordCount = wordsInSentenceCount; if (chunkWordCount > 0) { // add a new chunk containing all the input up to this sentence - chunks.add(input.substring(chunkStart, chunkEnd)); + chunks.add(new ChunkOffset(chunkStart, chunkEnd)); if (includePrecedingSentence) { if (wordsInPrecedingSentenceCount + wordsInSentenceCount > maxNumberWordsPerChunk) { @@ -127,12 +126,17 @@ public List chunk(String input, int maxNumberWordsPerChunk, boolean incl for (; i < sentenceSplits.size() - 1; i++) { // Because the substring was passed to splitLongSentence() // the returned positions need to be offset by chunkStart - chunks.add(input.substring(chunkStart + sentenceSplits.get(i).start(), chunkStart + sentenceSplits.get(i).end())); + chunks.add( + new ChunkOffset( + chunkStart + sentenceSplits.get(i).offsets().start(), + chunkStart + sentenceSplits.get(i).offsets().end() + ) + ); } // The final split is partially filled. // Set the next chunk start to the beginning of the // final split of the long sentence. - chunkStart = chunkStart + sentenceSplits.get(i).start(); // start pos needs to be offset by chunkStart + chunkStart = chunkStart + sentenceSplits.get(i).offsets().start(); // start pos needs to be offset by chunkStart chunkWordCount = sentenceSplits.get(i).wordCount(); } } else { @@ -151,7 +155,7 @@ public List chunk(String input, int maxNumberWordsPerChunk, boolean incl } if (chunkWordCount > 0) { - chunks.add(input.substring(chunkStart)); + chunks.add(new ChunkOffset(chunkStart, input.length())); } return chunks; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java index c9c752b9aabb..b15e2134f4cf 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; /** * Breaks text into smaller strings or chunks on Word boundaries. @@ -35,7 +36,7 @@ public WordBoundaryChunker() { wordIterator = BreakIterator.getWordInstance(Locale.ROOT); } - record ChunkPosition(int start, int end, int wordCount) {} + record ChunkPosition(ChunkOffset offsets, int wordCount) {} /** * Break the input text into small chunks as dictated @@ -45,7 +46,7 @@ record ChunkPosition(int start, int end, int wordCount) {} * @return List of chunked text */ @Override - public List chunk(String input, ChunkingSettings chunkingSettings) { + public List chunk(String input, ChunkingSettings chunkingSettings) { if (chunkingSettings instanceof WordBoundaryChunkingSettings wordBoundaryChunkerSettings) { return chunk(input, wordBoundaryChunkerSettings.maxChunkSize, wordBoundaryChunkerSettings.overlap); } else { @@ -64,18 +65,9 @@ public List chunk(String input, ChunkingSettings chunkingSettings) { * Can be 0 but must be non-negative. * @return List of chunked text */ - public List chunk(String input, int chunkSize, int overlap) { - - if (input.isEmpty()) { - return List.of(""); - } - + public List chunk(String input, int chunkSize, int overlap) { var chunkPositions = chunkPositions(input, chunkSize, overlap); - var chunks = new ArrayList(chunkPositions.size()); - for (var pos : chunkPositions) { - chunks.add(input.substring(pos.start, pos.end)); - } - return chunks; + return chunkPositions.stream().map(ChunkPosition::offsets).collect(Collectors.toList()); } /** @@ -127,7 +119,7 @@ List chunkPositions(String input, int chunkSize, int overlap) { wordsSinceStartWindowWasMarked++; if (wordsInChunkCountIncludingOverlap >= chunkSize) { - chunkPositions.add(new ChunkPosition(windowStart, boundary, wordsInChunkCountIncludingOverlap)); + chunkPositions.add(new ChunkPosition(new ChunkOffset(windowStart, boundary), wordsInChunkCountIncludingOverlap)); wordsInChunkCountIncludingOverlap = overlap; if (overlap == 0) { @@ -149,7 +141,7 @@ List chunkPositions(String input, int chunkSize, int overlap) { // if it ends on a boundary than the count should equal overlap in which case // we can ignore it, unless this is the first chunk in which case we want to add it if (wordsInChunkCountIncludingOverlap > overlap || chunkPositions.isEmpty()) { - chunkPositions.add(new ChunkPosition(windowStart, input.length(), wordsInChunkCountIncludingOverlap)); + chunkPositions.add(new ChunkPosition(new ChunkOffset(windowStart, input.length()), wordsInChunkCountIncludingOverlap)); } return chunkPositions; diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java index 4fdf254101d3..a82d2f474ca4 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java @@ -62,7 +62,7 @@ public void testMultipleShortInputsAreSingleBatch() { var subBatches = batches.get(0).batch().subBatches(); for (int i = 0; i < inputs.size(); i++) { var subBatch = subBatches.get(i); - assertThat(subBatch.requests(), contains(inputs.get(i))); + assertThat(subBatch.requests().toChunkText(), contains(inputs.get(i))); assertEquals(0, subBatch.positions().chunkIndex()); assertEquals(i, subBatch.positions().inputIndex()); assertEquals(1, subBatch.positions().embeddingCount()); @@ -102,7 +102,7 @@ public void testManyInputsMakeManyBatches() { var subBatches = batches.get(0).batch().subBatches(); for (int i = 0; i < batches.size(); i++) { var subBatch = subBatches.get(i); - assertThat(subBatch.requests(), contains(inputs.get(i))); + assertThat(subBatch.requests().toChunkText(), contains(inputs.get(i))); assertEquals(0, subBatch.positions().chunkIndex()); assertEquals(inputIndex, subBatch.positions().inputIndex()); assertEquals(1, subBatch.positions().embeddingCount()); @@ -146,7 +146,7 @@ public void testChunkingSettingsProvided() { var subBatches = batches.get(0).batch().subBatches(); for (int i = 0; i < batches.size(); i++) { var subBatch = subBatches.get(i); - assertThat(subBatch.requests(), contains(inputs.get(i))); + assertThat(subBatch.requests().toChunkText(), contains(inputs.get(i))); assertEquals(0, subBatch.positions().chunkIndex()); assertEquals(inputIndex, subBatch.positions().inputIndex()); assertEquals(1, subBatch.positions().embeddingCount()); @@ -184,17 +184,17 @@ public void testLongInputChunkedOverMultipleBatches() { assertEquals(0, subBatch.positions().inputIndex()); assertEquals(0, subBatch.positions().chunkIndex()); assertEquals(1, subBatch.positions().embeddingCount()); - assertThat(subBatch.requests(), contains("1st small")); + assertThat(subBatch.requests().toChunkText(), contains("1st small")); } { var subBatch = batch.subBatches().get(1); assertEquals(1, subBatch.positions().inputIndex()); // 2nd input assertEquals(0, subBatch.positions().chunkIndex()); // 1st part of the 2nd input assertEquals(4, subBatch.positions().embeddingCount()); // 4 chunks - assertThat(subBatch.requests().get(0), startsWith("passage_input0 ")); - assertThat(subBatch.requests().get(1), startsWith(" passage_input20 ")); - assertThat(subBatch.requests().get(2), startsWith(" passage_input40 ")); - assertThat(subBatch.requests().get(3), startsWith(" passage_input60 ")); + assertThat(subBatch.requests().toChunkText().get(0), startsWith("passage_input0 ")); + assertThat(subBatch.requests().toChunkText().get(1), startsWith(" passage_input20 ")); + assertThat(subBatch.requests().toChunkText().get(2), startsWith(" passage_input40 ")); + assertThat(subBatch.requests().toChunkText().get(3), startsWith(" passage_input60 ")); } } { @@ -207,22 +207,22 @@ public void testLongInputChunkedOverMultipleBatches() { assertEquals(1, subBatch.positions().inputIndex()); // 2nd input assertEquals(1, subBatch.positions().chunkIndex()); // 2nd part of the 2nd input assertEquals(2, subBatch.positions().embeddingCount()); - assertThat(subBatch.requests().get(0), startsWith(" passage_input80 ")); - assertThat(subBatch.requests().get(1), startsWith(" passage_input100 ")); + assertThat(subBatch.requests().toChunkText().get(0), startsWith(" passage_input80 ")); + assertThat(subBatch.requests().toChunkText().get(1), startsWith(" passage_input100 ")); } { var subBatch = batch.subBatches().get(1); assertEquals(2, subBatch.positions().inputIndex()); // 3rd input assertEquals(0, subBatch.positions().chunkIndex()); // 1st and only part assertEquals(1, subBatch.positions().embeddingCount()); // 1 chunk - assertThat(subBatch.requests(), contains("2nd small")); + assertThat(subBatch.requests().toChunkText(), contains("2nd small")); } { var subBatch = batch.subBatches().get(2); assertEquals(3, subBatch.positions().inputIndex()); // 4th input assertEquals(0, subBatch.positions().chunkIndex()); // 1st and only part assertEquals(1, subBatch.positions().embeddingCount()); // 1 chunk - assertThat(subBatch.requests(), contains("3rd small")); + assertThat(subBatch.requests().toChunkText(), contains("3rd small")); } } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java index afce8c57e035..de943f7f57ab 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java @@ -15,7 +15,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import static org.elasticsearch.xpack.inference.chunking.WordBoundaryChunkerTests.TEST_TEXT; import static org.hamcrest.Matchers.containsString; @@ -27,10 +29,24 @@ public class SentenceBoundaryChunkerTests extends ESTestCase { + /** + * Utility method for testing. + * Use the chunk functions that return offsets where possible + */ + private List textChunks( + SentenceBoundaryChunker chunker, + String input, + int maxNumberWordsPerChunk, + boolean includePrecedingSentence + ) { + var chunkPositions = chunker.chunk(input, maxNumberWordsPerChunk, includePrecedingSentence); + return chunkPositions.stream().map(offset -> input.substring(offset.start(), offset.end())).collect(Collectors.toList()); + } + public void testChunkSplitLargeChunkSizes() { for (int maxWordsPerChunk : new int[] { 100, 200 }) { var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(TEST_TEXT, maxWordsPerChunk, false); + var chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, false); int numChunks = expectedNumberOfChunks(sentenceSizes(TEST_TEXT), maxWordsPerChunk); assertThat("words per chunk " + maxWordsPerChunk, chunks, hasSize(numChunks)); @@ -48,7 +64,7 @@ public void testChunkSplitLargeChunkSizes_withOverlap() { boolean overlap = true; for (int maxWordsPerChunk : new int[] { 70, 80, 100, 120, 150, 200 }) { var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(TEST_TEXT, maxWordsPerChunk, overlap); + var chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, overlap); int[] overlaps = chunkOverlaps(sentenceSizes(TEST_TEXT), maxWordsPerChunk, overlap); assertThat("words per chunk " + maxWordsPerChunk, chunks, hasSize(overlaps.length)); @@ -107,7 +123,7 @@ public void testWithOverlap_SentencesFitInChunks() { } var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(sb.toString(), chunkSize, true); + var chunks = textChunks(chunker, sb.toString(), chunkSize, true); assertThat(chunks, hasSize(numChunks)); for (int i = 0; i < numChunks; i++) { assertThat("num sentences " + numSentences, chunks.get(i), startsWith("SStart" + sentenceStartIndexes[i])); @@ -128,10 +144,10 @@ private String makeSentence(int numWords, int sentenceIndex) { public void testChunk_ChunkSizeLargerThanText() { int maxWordsPerChunk = 500; var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(TEST_TEXT, maxWordsPerChunk, false); + var chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, false); assertEquals(chunks.get(0), TEST_TEXT); - chunks = chunker.chunk(TEST_TEXT, maxWordsPerChunk, true); + chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, true); assertEquals(chunks.get(0), TEST_TEXT); } @@ -142,7 +158,7 @@ public void testChunkSplit_SentencesLongerThanChunkSize() { for (int i = 0; i < chunkSizes.length; i++) { int maxWordsPerChunk = chunkSizes[i]; var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(TEST_TEXT, maxWordsPerChunk, false); + var chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, false); assertThat("words per chunk " + maxWordsPerChunk, chunks, hasSize(expectedNumberOFChunks[i])); for (var chunk : chunks) { @@ -171,7 +187,7 @@ public void testChunkSplit_SentencesLongerThanChunkSize_WithOverlap() { for (int i = 0; i < chunkSizes.length; i++) { int maxWordsPerChunk = chunkSizes[i]; var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(TEST_TEXT, maxWordsPerChunk, true); + var chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, true); assertThat(chunks.get(0), containsString("Word segmentation is the problem of dividing")); assertThat(chunks.get(chunks.size() - 1), containsString(", with solidification being a stronger norm.")); } @@ -190,7 +206,7 @@ public void testShortLongShortSentences_WithOverlap() { } var chunker = new SentenceBoundaryChunker(); - var chunks = chunker.chunk(sb.toString(), maxWordsPerChunk, true); + var chunks = textChunks(chunker, sb.toString(), maxWordsPerChunk, true); assertThat(chunks, hasSize(5)); assertTrue(chunks.get(0).trim().startsWith("SStart0")); // Entire sentence assertTrue(chunks.get(0).trim().endsWith(".")); // Entire sentence @@ -303,7 +319,7 @@ public void testChunkSplitLargeChunkSizesWithChunkingSettings() { for (int maxWordsPerChunk : new int[] { 100, 200 }) { var chunker = new SentenceBoundaryChunker(); SentenceBoundaryChunkingSettings chunkingSettings = new SentenceBoundaryChunkingSettings(maxWordsPerChunk, 0); - var chunks = chunker.chunk(TEST_TEXT, chunkingSettings); + var chunks = textChunks(chunker, TEST_TEXT, maxWordsPerChunk, false); int numChunks = expectedNumberOfChunks(sentenceSizes(TEST_TEXT), maxWordsPerChunk); assertThat("words per chunk " + maxWordsPerChunk, chunks, hasSize(numChunks)); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java index ef643a4b36fd..2ef28f2cf2e7 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; @@ -65,9 +66,22 @@ public class WordBoundaryChunkerTests extends ESTestCase { NUM_WORDS_IN_TEST_TEXT = wordCount; } + /** + * Utility method for testing. + * Use the chunk functions that return offsets where possible + */ + List textChunks(WordBoundaryChunker chunker, String input, int chunkSize, int overlap) { + if (input.isEmpty()) { + return List.of(""); + } + + var chunkPositions = chunker.chunk(input, chunkSize, overlap); + return chunkPositions.stream().map(p -> input.substring(p.start(), p.end())).collect(Collectors.toList()); + } + public void testSingleSplit() { var chunker = new WordBoundaryChunker(); - var chunks = chunker.chunk(TEST_TEXT, 10_000, 0); + var chunks = textChunks(chunker, TEST_TEXT, 10_000, 0); assertThat(chunks, hasSize(1)); assertEquals(TEST_TEXT, chunks.get(0)); } @@ -168,11 +182,11 @@ public void testWindowSpanningWords() { } var whiteSpacedText = input.toString().stripTrailing(); - var chunks = new WordBoundaryChunker().chunk(whiteSpacedText, 20, 10); + var chunks = textChunks(new WordBoundaryChunker(), whiteSpacedText, 20, 10); assertChunkContents(chunks, numWords, 20, 10); - chunks = new WordBoundaryChunker().chunk(whiteSpacedText, 10, 4); + chunks = textChunks(new WordBoundaryChunker(), whiteSpacedText, 10, 4); assertChunkContents(chunks, numWords, 10, 4); - chunks = new WordBoundaryChunker().chunk(whiteSpacedText, 15, 3); + chunks = textChunks(new WordBoundaryChunker(), whiteSpacedText, 15, 3); assertChunkContents(chunks, numWords, 15, 3); } @@ -217,28 +231,28 @@ public void testWindowSpanning_TextShorterThanWindow() { } public void testEmptyString() { - var chunks = new WordBoundaryChunker().chunk("", 10, 5); - assertThat(chunks, contains("")); + var chunks = textChunks(new WordBoundaryChunker(), "", 10, 5); + assertThat(chunks.toString(), chunks, contains("")); } public void testWhitespace() { - var chunks = new WordBoundaryChunker().chunk(" ", 10, 5); + var chunks = textChunks(new WordBoundaryChunker(), " ", 10, 5); assertThat(chunks, contains(" ")); } public void testPunctuation() { int chunkSize = 1; - var chunks = new WordBoundaryChunker().chunk("Comma, separated", chunkSize, 0); + var chunks = textChunks(new WordBoundaryChunker(), "Comma, separated", chunkSize, 0); assertThat(chunks, contains("Comma", ", separated")); - chunks = new WordBoundaryChunker().chunk("Mme. Thénardier", chunkSize, 0); + chunks = textChunks(new WordBoundaryChunker(), "Mme. Thénardier", chunkSize, 0); assertThat(chunks, contains("Mme", ". Thénardier")); - chunks = new WordBoundaryChunker().chunk("Won't you chunk", chunkSize, 0); + chunks = textChunks(new WordBoundaryChunker(), "Won't you chunk", chunkSize, 0); assertThat(chunks, contains("Won't", " you", " chunk")); chunkSize = 10; - chunks = new WordBoundaryChunker().chunk("Won't you chunk", chunkSize, 0); + chunks = textChunks(new WordBoundaryChunker(), "Won't you chunk", chunkSize, 0); assertThat(chunks, contains("Won't you chunk")); } From 22a392f1b69224aac3894dd6a05b892ccbb6a75d Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 9 Dec 2024 07:33:11 -0800 Subject: [PATCH 21/39] Remove client.type setting (#118192) The client.type setting is a holdover from the node client which was removed in 8.0. The setting has been a noop since 8.0. This commit removes the setting. relates #104574 --- docs/changelog/118192.yaml | 11 +++++++++++ .../org/elasticsearch/client/internal/Client.java | 10 ---------- .../common/settings/ClusterSettings.java | 2 -- 3 files changed, 11 insertions(+), 12 deletions(-) create mode 100644 docs/changelog/118192.yaml diff --git a/docs/changelog/118192.yaml b/docs/changelog/118192.yaml new file mode 100644 index 000000000000..03542048761d --- /dev/null +++ b/docs/changelog/118192.yaml @@ -0,0 +1,11 @@ +pr: 118192 +summary: Remove `client.type` setting +area: Infra/Core +type: breaking +issues: [104574] +breaking: + title: Remove `client.type` setting + area: Cluster and node setting + details: The node setting `client.type` has been ignored since the node client was removed in 8.0. The setting is now removed. + impact: Remove the `client.type` setting from `elasticsearch.yml` + notable: false diff --git a/server/src/main/java/org/elasticsearch/client/internal/Client.java b/server/src/main/java/org/elasticsearch/client/internal/Client.java index 4158bbfb27cd..2d1cbe0cce7f 100644 --- a/server/src/main/java/org/elasticsearch/client/internal/Client.java +++ b/server/src/main/java/org/elasticsearch/client/internal/Client.java @@ -52,8 +52,6 @@ import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateRequestBuilder; import org.elasticsearch.action.update.UpdateResponse; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Nullable; import org.elasticsearch.transport.RemoteClusterService; @@ -74,14 +72,6 @@ */ public interface Client extends ElasticsearchClient { - // Note: This setting is registered only for bwc. The value is never read. - Setting CLIENT_TYPE_SETTING_S = new Setting<>("client.type", "node", (s) -> { - return switch (s) { - case "node", "transport" -> s; - default -> throw new IllegalArgumentException("Can't parse [client.type] must be one of [node, transport]"); - }; - }, Property.NodeScope, Property.Deprecated); - /** * The admin client that can be used to perform administrative operations. */ diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index a9a9411de8e1..16af7ca2915d 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -20,7 +20,6 @@ import org.elasticsearch.action.support.DestructiveOperations; import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.bootstrap.BootstrapSettings; -import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterModule; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.InternalClusterInfoService; @@ -483,7 +482,6 @@ public void apply(Settings value, Settings current, Settings previous) { AutoCreateIndex.AUTO_CREATE_INDEX_SETTING, BaseRestHandler.MULTI_ALLOW_EXPLICIT_INDEX, ClusterName.CLUSTER_NAME_SETTING, - Client.CLIENT_TYPE_SETTING_S, ClusterModule.SHARDS_ALLOCATOR_TYPE_SETTING, EsExecutors.NODE_PROCESSORS_SETTING, ThreadContext.DEFAULT_HEADERS_SETTING, From 0586cbfb34c7201a996578db60d12fea8594261c Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 9 Dec 2024 15:46:22 +0000 Subject: [PATCH 22/39] Remove unused `BlobStore#deleteBlobsIgnoringIfNotExists` (#118245) This method is never called against a general `BlobStore`, we only use it in certain implementations for which a bulk delete at the `BlobStore` level makes sense. This commit removes the unused interface method. --- .../azure/AzureBlobContainer.java | 2 +- .../repositories/azure/AzureBlobStore.java | 3 +- .../azure/AzureBlobContainerStatsTests.java | 9 ++--- .../gcs/GoogleCloudStorageBlobContainer.java | 4 +-- .../gcs/GoogleCloudStorageBlobStore.java | 10 ++---- .../repositories/s3/S3BlobContainer.java | 6 ++-- .../repositories/s3/S3BlobStore.java | 3 +- .../common/blobstore/url/URLBlobStore.java | 8 ----- .../repositories/hdfs/HdfsBlobStore.java | 7 ---- .../hdfs/HdfsBlobStoreRepositoryTests.java | 5 --- ...BlobStoreRepositoryOperationPurposeIT.java | 6 ---- .../common/blobstore/BlobStore.java | 10 ------ .../common/blobstore/fs/FsBlobContainer.java | 2 +- .../common/blobstore/fs/FsBlobStore.java | 4 +-- ...bStoreRepositoryDeleteThrottlingTests.java | 6 ---- .../LatencySimulatingBlobStoreRepository.java | 6 ---- .../ESBlobStoreRepositoryIntegTestCase.java | 35 ------------------- .../snapshots/mockstore/BlobStoreWrapper.java | 9 +---- ...archableSnapshotsPrewarmingIntegTests.java | 7 ---- .../analyze/RepositoryAnalysisFailureIT.java | 5 --- .../analyze/RepositoryAnalysisSuccessIT.java | 5 --- 21 files changed, 17 insertions(+), 135 deletions(-) diff --git a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java index 73936d82fc20..08bdc2051b9e 100644 --- a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java +++ b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobContainer.java @@ -144,7 +144,7 @@ public DeleteResult delete(OperationPurpose purpose) throws IOException { @Override public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - blobStore.deleteBlobsIgnoringIfNotExists(purpose, new Iterator<>() { + blobStore.deleteBlobs(purpose, new Iterator<>() { @Override public boolean hasNext() { return blobNames.hasNext(); diff --git a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobStore.java b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobStore.java index e4f973fb73a4..3cac0dc4bb6d 100644 --- a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobStore.java +++ b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobStore.java @@ -264,8 +264,7 @@ public DeleteResult deleteBlobDirectory(OperationPurpose purpose, String path) t return new DeleteResult(blobsDeleted.get(), bytesDeleted.get()); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { + void deleteBlobs(OperationPurpose purpose, Iterator blobNames) { if (blobNames.hasNext() == false) { return; } diff --git a/modules/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerStatsTests.java b/modules/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerStatsTests.java index f6e97187222e..8979507230bd 100644 --- a/modules/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerStatsTests.java +++ b/modules/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerStatsTests.java @@ -72,7 +72,7 @@ public void testRetriesAndOperationsAreTrackedSeparately() throws IOException { false ); case LIST_BLOBS -> blobStore.listBlobsByPrefix(purpose, randomIdentifier(), randomIdentifier()); - case BLOB_BATCH -> blobStore.deleteBlobsIgnoringIfNotExists( + case BLOB_BATCH -> blobStore.deleteBlobs( purpose, List.of(randomIdentifier(), randomIdentifier(), randomIdentifier()).iterator() ); @@ -113,7 +113,7 @@ public void testOperationPurposeIsReflectedInBlobStoreStats() throws IOException os.flush(); }); // BLOB_BATCH - blobStore.deleteBlobsIgnoringIfNotExists(purpose, List.of(randomIdentifier(), randomIdentifier(), randomIdentifier()).iterator()); + blobStore.deleteBlobs(purpose, List.of(randomIdentifier(), randomIdentifier(), randomIdentifier()).iterator()); Map stats = blobStore.stats(); String statsMapString = stats.toString(); @@ -148,10 +148,7 @@ public void testOperationPurposeIsNotReflectedInBlobStoreStatsWhenNotServerless( os.flush(); }); // BLOB_BATCH - blobStore.deleteBlobsIgnoringIfNotExists( - purpose, - List.of(randomIdentifier(), randomIdentifier(), randomIdentifier()).iterator() - ); + blobStore.deleteBlobs(purpose, List.of(randomIdentifier(), randomIdentifier(), randomIdentifier()).iterator()); } Map stats = blobStore.stats(); diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java index 047549cc893e..edcf03580da0 100644 --- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java +++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainer.java @@ -114,12 +114,12 @@ public void writeBlobAtomic(OperationPurpose purpose, String blobName, BytesRefe @Override public DeleteResult delete(OperationPurpose purpose) throws IOException { - return blobStore.deleteDirectory(purpose, path().buildAsString()); + return blobStore.deleteDirectory(path().buildAsString()); } @Override public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - blobStore.deleteBlobsIgnoringIfNotExists(purpose, new Iterator<>() { + blobStore.deleteBlobs(new Iterator<>() { @Override public boolean hasNext() { return blobNames.hasNext(); diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java index 9cbf64e7e014..c68217a1a373 100644 --- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java +++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java @@ -29,7 +29,6 @@ import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.blobstore.BlobStoreActionStats; import org.elasticsearch.common.blobstore.DeleteResult; -import org.elasticsearch.common.blobstore.OperationPurpose; import org.elasticsearch.common.blobstore.OptionalBytesReference; import org.elasticsearch.common.blobstore.support.BlobContainerUtils; import org.elasticsearch.common.blobstore.support.BlobMetadata; @@ -491,10 +490,9 @@ private void writeBlobMultipart(BlobInfo blobInfo, byte[] buffer, int offset, in /** * Deletes the given path and all its children. * - * @param purpose The purpose of the delete operation * @param pathStr Name of path to delete */ - DeleteResult deleteDirectory(OperationPurpose purpose, String pathStr) throws IOException { + DeleteResult deleteDirectory(String pathStr) throws IOException { return SocketAccess.doPrivilegedIOException(() -> { DeleteResult deleteResult = DeleteResult.ZERO; Page page = client().list(bucketName, BlobListOption.prefix(pathStr)); @@ -502,7 +500,7 @@ DeleteResult deleteDirectory(OperationPurpose purpose, String pathStr) throws IO final AtomicLong blobsDeleted = new AtomicLong(0L); final AtomicLong bytesDeleted = new AtomicLong(0L); final Iterator blobs = page.getValues().iterator(); - deleteBlobsIgnoringIfNotExists(purpose, new Iterator<>() { + deleteBlobs(new Iterator<>() { @Override public boolean hasNext() { return blobs.hasNext(); @@ -526,11 +524,9 @@ public String next() { /** * Deletes multiple blobs from the specific bucket using a batch request * - * @param purpose the purpose of the delete operation * @param blobNames names of the blobs to delete */ - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { + void deleteBlobs(Iterator blobNames) throws IOException { if (blobNames.hasNext() == false) { return; } diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java index e13cc40dd3e0..f527dcd42814 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java @@ -342,10 +342,10 @@ public DeleteResult delete(OperationPurpose purpose) throws IOException { return summary.getKey(); }); if (list.isTruncated()) { - blobStore.deleteBlobsIgnoringIfNotExists(purpose, blobNameIterator); + blobStore.deleteBlobs(purpose, blobNameIterator); prevListing = list; } else { - blobStore.deleteBlobsIgnoringIfNotExists(purpose, Iterators.concat(blobNameIterator, Iterators.single(keyPath))); + blobStore.deleteBlobs(purpose, Iterators.concat(blobNameIterator, Iterators.single(keyPath))); break; } } @@ -357,7 +357,7 @@ public DeleteResult delete(OperationPurpose purpose) throws IOException { @Override public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - blobStore.deleteBlobsIgnoringIfNotExists(purpose, Iterators.map(blobNames, this::buildKey)); + blobStore.deleteBlobs(purpose, Iterators.map(blobNames, this::buildKey)); } @Override diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java index 4f2b0f213e44..4bd54aa37077 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java @@ -340,8 +340,7 @@ public BlobContainer blobContainer(BlobPath path) { return new S3BlobContainer(path, this); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { + void deleteBlobs(OperationPurpose purpose, Iterator blobNames) throws IOException { if (blobNames.hasNext() == false) { return; } diff --git a/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobStore.java b/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobStore.java index 8538d2ba673b..0e9c735b22fd 100644 --- a/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobStore.java +++ b/modules/repository-url/src/main/java/org/elasticsearch/common/blobstore/url/URLBlobStore.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.blobstore.BlobStoreException; -import org.elasticsearch.common.blobstore.OperationPurpose; import org.elasticsearch.common.blobstore.url.http.HttpURLBlobContainer; import org.elasticsearch.common.blobstore.url.http.URLHttpClient; import org.elasticsearch.common.blobstore.url.http.URLHttpClientSettings; @@ -23,10 +22,8 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.CheckedFunction; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.util.Iterator; import java.util.List; /** @@ -109,11 +106,6 @@ public BlobContainer blobContainer(BlobPath blobPath) { } } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - throw new UnsupportedOperationException("Bulk deletes are not supported in URL repositories"); - } - @Override public void close() { // nothing to do here... diff --git a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobStore.java b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobStore.java index eaf2429ae625..e817384d95c0 100644 --- a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobStore.java +++ b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsBlobStore.java @@ -16,10 +16,8 @@ import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; -import org.elasticsearch.common.blobstore.OperationPurpose; import java.io.IOException; -import java.util.Iterator; final class HdfsBlobStore implements BlobStore { @@ -72,11 +70,6 @@ public BlobContainer blobContainer(BlobPath path) { return new HdfsBlobContainer(path, this, buildHdfsPath(path), bufferSize, securityContext, replicationFactor); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - throw new UnsupportedOperationException("Bulk deletes are not supported in Hdfs repositories"); - } - private Path buildHdfsPath(BlobPath blobPath) { final Path path = translateToHdfsPath(blobPath); if (readOnly == false) { diff --git a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java index 17927b02a08d..3e1c112a4d9f 100644 --- a/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java +++ b/plugins/repository-hdfs/src/test/java/org/elasticsearch/repositories/hdfs/HdfsBlobStoreRepositoryTests.java @@ -46,11 +46,6 @@ public void testSnapshotAndRestore() throws Exception { testSnapshotAndRestore(false); } - @Override - public void testBlobStoreBulkDeletion() throws Exception { - // HDFS does not implement bulk deletion from different BlobContainers - } - @Override protected Collection> nodePlugins() { return Collections.singletonList(HdfsPlugin.class); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryOperationPurposeIT.java b/server/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryOperationPurposeIT.java index c0a2c83f7fe1..b2e02b2f4c27 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryOperationPurposeIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryOperationPurposeIT.java @@ -36,7 +36,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Collection; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -136,11 +135,6 @@ public BlobContainer blobContainer(BlobPath path) { return new AssertingBlobContainer(delegateBlobStore.blobContainer(path)); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - delegateBlobStore.deleteBlobsIgnoringIfNotExists(purpose, blobNames); - } - @Override public void close() throws IOException { delegateBlobStore.close(); diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java b/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java index d67c034fd3e2..f1fe028f60f6 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java @@ -9,9 +9,7 @@ package org.elasticsearch.common.blobstore; import java.io.Closeable; -import java.io.IOException; import java.util.Collections; -import java.util.Iterator; import java.util.Map; /** @@ -28,14 +26,6 @@ public interface BlobStore extends Closeable { */ BlobContainer blobContainer(BlobPath path); - /** - * Delete all the provided blobs from the blob store. Each blob could belong to a different {@code BlobContainer} - * - * @param purpose the purpose of the delete operation - * @param blobNames the blobs to be deleted - */ - void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException; - /** * Returns statistics on the count of operations that have been performed on this blob store */ diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java b/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java index b5118d8a289a..7d4000823129 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobContainer.java @@ -177,7 +177,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO @Override public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - blobStore.deleteBlobsIgnoringIfNotExists(purpose, Iterators.map(blobNames, blobName -> path.resolve(blobName).toString())); + blobStore.deleteBlobs(Iterators.map(blobNames, blobName -> path.resolve(blobName).toString())); } @Override diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobStore.java b/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobStore.java index 53e3b4b4796d..9a368483d46c 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobStore.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/fs/FsBlobStore.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; -import org.elasticsearch.common.blobstore.OperationPurpose; import org.elasticsearch.core.IOUtils; import java.io.IOException; @@ -70,8 +69,7 @@ public BlobContainer blobContainer(BlobPath path) { return new FsBlobContainer(this, path, f); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { + void deleteBlobs(Iterator blobNames) throws IOException { IOException ioe = null; long suppressedExceptions = 0; while (blobNames.hasNext()) { diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryDeleteThrottlingTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryDeleteThrottlingTests.java index 0b5999b61405..4facaa391ec2 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryDeleteThrottlingTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryDeleteThrottlingTests.java @@ -35,7 +35,6 @@ import java.io.OutputStream; import java.util.Collection; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -100,11 +99,6 @@ public BlobContainer blobContainer(BlobPath path) { return new ConcurrencyLimitingBlobContainer(delegate.blobContainer(path), activeIndices, countDownLatch); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - delegate.deleteBlobsIgnoringIfNotExists(purpose, blobNames); - } - @Override public void close() throws IOException { delegate.close(); diff --git a/test/external-modules/latency-simulating-directory/src/main/java/org/elasticsearch/test/simulatedlatencyrepo/LatencySimulatingBlobStoreRepository.java b/test/external-modules/latency-simulating-directory/src/main/java/org/elasticsearch/test/simulatedlatencyrepo/LatencySimulatingBlobStoreRepository.java index f360a6c012cb..cd2812a95cfa 100644 --- a/test/external-modules/latency-simulating-directory/src/main/java/org/elasticsearch/test/simulatedlatencyrepo/LatencySimulatingBlobStoreRepository.java +++ b/test/external-modules/latency-simulating-directory/src/main/java/org/elasticsearch/test/simulatedlatencyrepo/LatencySimulatingBlobStoreRepository.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Iterator; class LatencySimulatingBlobStoreRepository extends FsRepository { @@ -53,11 +52,6 @@ public BlobContainer blobContainer(BlobPath path) { return new LatencySimulatingBlobContainer(blobContainer); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - fsBlobStore.deleteBlobsIgnoringIfNotExists(purpose, blobNames); - } - @Override public void close() throws IOException { fsBlobStore.close(); diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java index b85ee970664e..c982f36e5ccb 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESBlobStoreRepositoryIntegTestCase.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.NoSuchFileException; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -70,7 +69,6 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -524,39 +522,6 @@ public void testIndicesDeletedFromRepository() throws Exception { assertAcked(clusterAdmin().prepareDeleteSnapshot(TEST_REQUEST_TIMEOUT, repoName, "test-snap2").get()); } - public void testBlobStoreBulkDeletion() throws Exception { - Map> expectedBlobsPerContainer = new HashMap<>(); - try (BlobStore store = newBlobStore()) { - List blobsToDelete = new ArrayList<>(); - int numberOfContainers = randomIntBetween(2, 5); - for (int i = 0; i < numberOfContainers; i++) { - BlobPath containerPath = BlobPath.EMPTY.add(randomIdentifier()); - final BlobContainer container = store.blobContainer(containerPath); - int numberOfBlobsPerContainer = randomIntBetween(5, 10); - for (int j = 0; j < numberOfBlobsPerContainer; j++) { - byte[] bytes = randomBytes(randomInt(100)); - String blobName = randomAlphaOfLength(10); - container.writeBlob(randomPurpose(), blobName, new BytesArray(bytes), false); - if (randomBoolean()) { - blobsToDelete.add(containerPath.buildAsString() + blobName); - } else { - expectedBlobsPerContainer.computeIfAbsent(containerPath, unused -> new ArrayList<>()).add(blobName); - } - } - } - - store.deleteBlobsIgnoringIfNotExists(randomPurpose(), blobsToDelete.iterator()); - for (var containerEntry : expectedBlobsPerContainer.entrySet()) { - BlobContainer blobContainer = store.blobContainer(containerEntry.getKey()); - Map blobsInContainer = blobContainer.listBlobs(randomPurpose()); - for (String expectedBlob : containerEntry.getValue()) { - assertThat(blobsInContainer, hasKey(expectedBlob)); - } - blobContainer.delete(randomPurpose()); - } - } - } - public void testDanglingShardLevelBlobCleanup() throws Exception { final var repoName = createRepository(randomRepositoryName()); final var client = client(); diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobStoreWrapper.java b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobStoreWrapper.java index 5803c2a82567..54af75fc584d 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobStoreWrapper.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/mockstore/BlobStoreWrapper.java @@ -12,15 +12,13 @@ import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.blobstore.BlobStoreActionStats; -import org.elasticsearch.common.blobstore.OperationPurpose; import java.io.IOException; -import java.util.Iterator; import java.util.Map; public class BlobStoreWrapper implements BlobStore { - private BlobStore delegate; + private final BlobStore delegate; public BlobStoreWrapper(BlobStore delegate) { this.delegate = delegate; @@ -31,11 +29,6 @@ public BlobContainer blobContainer(BlobPath path) { return delegate.blobContainer(path); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) throws IOException { - delegate.deleteBlobsIgnoringIfNotExists(purpose, blobNames); - } - @Override public void close() throws IOException { delegate.close(); diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/SearchableSnapshotsPrewarmingIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/SearchableSnapshotsPrewarmingIntegTests.java index ab38a8987050..c955457b78d6 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/SearchableSnapshotsPrewarmingIntegTests.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/SearchableSnapshotsPrewarmingIntegTests.java @@ -67,7 +67,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -466,12 +465,6 @@ public BlobContainer blobContainer(BlobPath path) { return new TrackingFilesBlobContainer(delegate.blobContainer(path)); } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) - throws IOException { - delegate.deleteBlobsIgnoringIfNotExists(purpose, blobNames); - } - @Override public void close() throws IOException { delegate.close(); diff --git a/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisFailureIT.java b/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisFailureIT.java index b8acd9808a35..6a638f53a633 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisFailureIT.java +++ b/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisFailureIT.java @@ -569,11 +569,6 @@ public BlobContainer blobContainer(BlobPath path) { } } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) { - assertPurpose(purpose); - } - private void deleteContainer(DisruptableBlobContainer container) { blobContainer = null; } diff --git a/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisSuccessIT.java b/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisSuccessIT.java index 1f8b247e7617..c24a254d34ac 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisSuccessIT.java +++ b/x-pack/plugin/snapshot-repo-test-kit/src/internalClusterTest/java/org/elasticsearch/repositories/blobstore/testkit/analyze/RepositoryAnalysisSuccessIT.java @@ -287,11 +287,6 @@ private void deleteContainer(AssertingBlobContainer container) { } } - @Override - public void deleteBlobsIgnoringIfNotExists(OperationPurpose purpose, Iterator blobNames) { - assertPurpose(purpose); - } - @Override public void close() {} From 5e859d9301ffe736548dfc2b6e72807a7f9006ff Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 9 Dec 2024 11:06:27 -0500 Subject: [PATCH 23/39] Even better(er) binary quantization (#117994) This measurably improves BBQ by adjusting the underlying algorithm to an optimized per vector scalar quantization. This is a brand new way to quantize vectors. Instead of there being a global set of upper and lower quantile bands, these are optimized and calculated per individual vector. Additionally, vectors are centered on a common centroid. This allows for an almost 32x reduction in memory, and even better recall than before at the cost of slightly increasing indexing time. Additionally, this new approach is easily generalizable to various other bit sizes (e.g. 2 bits, etc.). While not taken advantage of yet, we may update our scalar quantized indices in the future to use this new algorithm, giving significant boosts in recall. The recall gains spread from 2% to almost 10% for certain datasets with an additional 5-10% indexing cost when indexing with HNSW when compared with current BBQ. --- docs/changelog/117994.yaml | 5 + rest-api-spec/build.gradle | 2 + .../search.vectors/41_knn_search_bbq_hnsw.yml | 66 +- .../search.vectors/42_knn_search_bbq_flat.yml | 66 +- server/src/main/java/module-info.java | 4 +- .../index/codec/vectors/BQVectorUtils.java | 14 + .../es816/ES816BinaryFlatVectorsScorer.java | 59 +- .../ES816BinaryQuantizedVectorsFormat.java | 2 +- ...ES816HnswBinaryQuantizedVectorsFormat.java | 17 +- .../es818/BinarizedByteVectorValues.java | 87 ++ .../es818/ES818BinaryFlatVectorsScorer.java | 188 ++++ .../ES818BinaryQuantizedVectorsFormat.java | 132 +++ .../ES818BinaryQuantizedVectorsReader.java | 412 ++++++++ .../ES818BinaryQuantizedVectorsWriter.java | 944 ++++++++++++++++++ ...ES818HnswBinaryQuantizedVectorsFormat.java | 145 +++ .../es818/OffHeapBinarizedVectorValues.java | 371 +++++++ .../es818/OptimizedScalarQuantizer.java | 246 +++++ .../vectors/DenseVectorFieldMapper.java | 8 +- .../action/search/SearchCapabilities.java | 2 + .../org.apache.lucene.codecs.KnnVectorsFormat | 2 + .../codec/vectors/BQVectorUtilsTests.java | 26 + .../es816/ES816BinaryFlatRWVectorsScorer.java | 256 +++++ .../ES816BinaryFlatVectorsScorerTests.java | 12 +- .../ES816BinaryQuantizedRWVectorsFormat.java | 52 + ...S816BinaryQuantizedVectorsFormatTests.java | 2 +- .../ES816BinaryQuantizedVectorsWriter.java | 4 +- ...816HnswBinaryQuantizedRWVectorsFormat.java | 55 + ...HnswBinaryQuantizedVectorsFormatTests.java | 2 +- ...S818BinaryQuantizedVectorsFormatTests.java | 181 ++++ ...HnswBinaryQuantizedVectorsFormatTests.java | 132 +++ .../es818/OptimizedScalarQuantizerTests.java | 136 +++ .../vectors/DenseVectorFieldMapperTests.java | 8 +- 32 files changed, 3501 insertions(+), 137 deletions(-) create mode 100644 docs/changelog/117994.yaml create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/BinarizedByteVectorValues.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryFlatVectorsScorer.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsReader.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsWriter.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OffHeapBinarizedVectorValues.java create mode 100644 server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizer.java create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatRWVectorsScorer.java create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedRWVectorsFormat.java rename server/src/{main => test}/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java (99%) create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedRWVectorsFormat.java create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizerTests.java diff --git a/docs/changelog/117994.yaml b/docs/changelog/117994.yaml new file mode 100644 index 000000000000..603f2d855a11 --- /dev/null +++ b/docs/changelog/117994.yaml @@ -0,0 +1,5 @@ +pr: 117994 +summary: Even better(er) binary quantization +area: Vector Search +type: enhancement +issues: [] diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle index e2af894eb093..7347d9c1312d 100644 --- a/rest-api-spec/build.gradle +++ b/rest-api-spec/build.gradle @@ -67,4 +67,6 @@ tasks.named("yamlRestCompatTestTransform").configure ({ task -> task.skipTest("logsdb/20_source_mapping/include/exclude is supported with stored _source", "no longer serialize source_mode") task.skipTest("logsdb/20_source_mapping/synthetic _source is default", "no longer serialize source_mode") task.skipTest("search/520_fetch_fields/fetch _seq_no via fields", "error code is changed from 5xx to 400 in 9.0") + task.skipTest("search.vectors/41_knn_search_bbq_hnsw/Test knn search", "Scoring has changed in latest versions") + task.skipTest("search.vectors/42_knn_search_bbq_flat/Test knn search", "Scoring has changed in latest versions") }) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml index 188c155e4a83..5767c895fbe7 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_bbq_hnsw.yml @@ -11,20 +11,11 @@ setup: number_of_shards: 1 mappings: properties: - name: - type: keyword vector: type: dense_vector dims: 64 index: true - similarity: l2_norm - index_options: - type: bbq_hnsw - another_vector: - type: dense_vector - dims: 64 - index: true - similarity: l2_norm + similarity: max_inner_product index_options: type: bbq_hnsw @@ -33,9 +24,14 @@ setup: index: bbq_hnsw id: "1" body: - name: cow.jpg - vector: [300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0] - another_vector: [115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0] + vector: [0.077, 0.32 , -0.205, 0.63 , 0.032, 0.201, 0.167, -0.313, + 0.176, 0.531, -0.375, 0.334, -0.046, 0.078, -0.349, 0.272, + 0.307, -0.083, 0.504, 0.255, -0.404, 0.289, -0.226, -0.132, + -0.216, 0.49 , 0.039, 0.507, -0.307, 0.107, 0.09 , -0.265, + -0.285, 0.336, -0.272, 0.369, -0.282, 0.086, -0.132, 0.475, + -0.224, 0.203, 0.439, 0.064, 0.246, -0.396, 0.297, 0.242, + -0.028, 0.321, -0.022, -0.009, -0.001 , 0.031, -0.533, 0.45, + -0.683, 1.331, 0.194, -0.157, -0.1 , -0.279, -0.098, -0.176] # Flush in order to provoke a merge later - do: indices.flush: @@ -46,9 +42,14 @@ setup: index: bbq_hnsw id: "2" body: - name: moose.jpg - vector: [100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0] - another_vector: [50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120] + vector: [0.196, 0.514, 0.039, 0.555, -0.042, 0.242, 0.463, -0.348, + -0.08 , 0.442, -0.067, -0.05 , -0.001, 0.298, -0.377, 0.048, + 0.307, 0.159, 0.278, 0.119, -0.057, 0.333, -0.289, -0.438, + -0.014, 0.361, -0.169, 0.292, -0.229, 0.123, 0.031, -0.138, + -0.139, 0.315, -0.216, 0.322, -0.445, -0.059, 0.071, 0.429, + -0.602, -0.142, 0.11 , 0.192, 0.259, -0.241, 0.181, -0.166, + 0.082, 0.107, -0.05 , 0.155, 0.011, 0.161, -0.486, 0.569, + -0.489, 0.901, 0.208, 0.011, -0.209, -0.153, -0.27 , -0.013] # Flush in order to provoke a merge later - do: indices.flush: @@ -60,8 +61,14 @@ setup: id: "3" body: name: rabbit.jpg - vector: [111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0] - another_vector: [11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0] + vector: [0.139, 0.178, -0.117, 0.399, 0.014, -0.139, 0.347, -0.33 , + 0.139, 0.34 , -0.052, -0.052, -0.249, 0.327, -0.288, 0.049, + 0.464, 0.338, 0.516, 0.247, -0.104, 0.259, -0.209, -0.246, + -0.11 , 0.323, 0.091, 0.442, -0.254, 0.195, -0.109, -0.058, + -0.279, 0.402, -0.107, 0.308, -0.273, 0.019, 0.082, 0.399, + -0.658, -0.03 , 0.276, 0.041, 0.187, -0.331, 0.165, 0.017, + 0.171, -0.203, -0.198, 0.115, -0.007, 0.337, -0.444, 0.615, + -0.657, 1.285, 0.2 , -0.062, 0.038, 0.089, -0.068, -0.058] # Flush in order to provoke a merge later - do: indices.flush: @@ -73,20 +80,33 @@ setup: max_num_segments: 1 --- "Test knn search": + - requires: + capabilities: + - method: POST + path: /_search + capabilities: [ optimized_scalar_quantization_bbq ] + test_runner_features: capabilities + reason: "BBQ scoring improved and changed with optimized_scalar_quantization_bbq" - do: search: index: bbq_hnsw body: knn: field: vector - query_vector: [ 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0] + query_vector: [0.128, 0.067, -0.08 , 0.395, -0.11 , -0.259, 0.473, -0.393, + 0.292, 0.571, -0.491, 0.444, -0.288, 0.198, -0.343, 0.015, + 0.232, 0.088, 0.228, 0.151, -0.136, 0.236, -0.273, -0.259, + -0.217, 0.359, -0.207, 0.352, -0.142, 0.192, -0.061, -0.17 , + -0.343, 0.189, -0.221, 0.32 , -0.301, -0.1 , 0.005, 0.232, + -0.344, 0.136, 0.252, 0.157, -0.13 , -0.244, 0.193, -0.034, + -0.12 , -0.193, -0.102, 0.252, -0.185, -0.167, -0.575, 0.582, + -0.426, 0.983, 0.212, 0.204, 0.03 , -0.276, -0.425, -0.158] k: 3 num_candidates: 3 - # Depending on how things are distributed, docs 2 and 3 might be swapped - # here we verify that are last hit is always the worst one - - match: { hits.hits.2._id: "1" } - + - match: { hits.hits.0._id: "1" } + - match: { hits.hits.1._id: "3" } + - match: { hits.hits.2._id: "2" } --- "Test bad quantization parameters": - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/42_knn_search_bbq_flat.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/42_knn_search_bbq_flat.yml index ed7a8dd5df65..dcdae04aeabb 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/42_knn_search_bbq_flat.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/42_knn_search_bbq_flat.yml @@ -11,20 +11,11 @@ setup: number_of_shards: 1 mappings: properties: - name: - type: keyword vector: type: dense_vector dims: 64 index: true - similarity: l2_norm - index_options: - type: bbq_flat - another_vector: - type: dense_vector - dims: 64 - index: true - similarity: l2_norm + similarity: max_inner_product index_options: type: bbq_flat @@ -33,9 +24,14 @@ setup: index: bbq_flat id: "1" body: - name: cow.jpg - vector: [300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0, 230.0, 300.33, -34.8988, 15.555, -200.0] - another_vector: [115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0, 130.0, 115.0, -1.02, 15.555, -100.0] + vector: [0.077, 0.32 , -0.205, 0.63 , 0.032, 0.201, 0.167, -0.313, + 0.176, 0.531, -0.375, 0.334, -0.046, 0.078, -0.349, 0.272, + 0.307, -0.083, 0.504, 0.255, -0.404, 0.289, -0.226, -0.132, + -0.216, 0.49 , 0.039, 0.507, -0.307, 0.107, 0.09 , -0.265, + -0.285, 0.336, -0.272, 0.369, -0.282, 0.086, -0.132, 0.475, + -0.224, 0.203, 0.439, 0.064, 0.246, -0.396, 0.297, 0.242, + -0.028, 0.321, -0.022, -0.009, -0.001 , 0.031, -0.533, 0.45, + -0.683, 1.331, 0.194, -0.157, -0.1 , -0.279, -0.098, -0.176] # Flush in order to provoke a merge later - do: indices.flush: @@ -46,9 +42,14 @@ setup: index: bbq_flat id: "2" body: - name: moose.jpg - vector: [100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0, -0.5, 100.0, -13, 14.8, -156.0] - another_vector: [50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120, -0.5, 50.0, -1, 1, 120] + vector: [0.196, 0.514, 0.039, 0.555, -0.042, 0.242, 0.463, -0.348, + -0.08 , 0.442, -0.067, -0.05 , -0.001, 0.298, -0.377, 0.048, + 0.307, 0.159, 0.278, 0.119, -0.057, 0.333, -0.289, -0.438, + -0.014, 0.361, -0.169, 0.292, -0.229, 0.123, 0.031, -0.138, + -0.139, 0.315, -0.216, 0.322, -0.445, -0.059, 0.071, 0.429, + -0.602, -0.142, 0.11 , 0.192, 0.259, -0.241, 0.181, -0.166, + 0.082, 0.107, -0.05 , 0.155, 0.011, 0.161, -0.486, 0.569, + -0.489, 0.901, 0.208, 0.011, -0.209, -0.153, -0.27 , -0.013] # Flush in order to provoke a merge later - do: indices.flush: @@ -59,9 +60,14 @@ setup: index: bbq_flat id: "3" body: - name: rabbit.jpg - vector: [111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0, 0.5, 111.3, -13.0, 14.8, -156.0] - another_vector: [11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0, -0.5, 11.0, 0, 12, 111.0] + vector: [0.139, 0.178, -0.117, 0.399, 0.014, -0.139, 0.347, -0.33 , + 0.139, 0.34 , -0.052, -0.052, -0.249, 0.327, -0.288, 0.049, + 0.464, 0.338, 0.516, 0.247, -0.104, 0.259, -0.209, -0.246, + -0.11 , 0.323, 0.091, 0.442, -0.254, 0.195, -0.109, -0.058, + -0.279, 0.402, -0.107, 0.308, -0.273, 0.019, 0.082, 0.399, + -0.658, -0.03 , 0.276, 0.041, 0.187, -0.331, 0.165, 0.017, + 0.171, -0.203, -0.198, 0.115, -0.007, 0.337, -0.444, 0.615, + -0.657, 1.285, 0.2 , -0.062, 0.038, 0.089, -0.068, -0.058] # Flush in order to provoke a merge later - do: indices.flush: @@ -73,19 +79,33 @@ setup: max_num_segments: 1 --- "Test knn search": + - requires: + capabilities: + - method: POST + path: /_search + capabilities: [ optimized_scalar_quantization_bbq ] + test_runner_features: capabilities + reason: "BBQ scoring improved and changed with optimized_scalar_quantization_bbq" - do: search: index: bbq_flat body: knn: field: vector - query_vector: [ 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0, -0.5, 90.0, -10, 14.8, -156.0] + query_vector: [0.128, 0.067, -0.08 , 0.395, -0.11 , -0.259, 0.473, -0.393, + 0.292, 0.571, -0.491, 0.444, -0.288, 0.198, -0.343, 0.015, + 0.232, 0.088, 0.228, 0.151, -0.136, 0.236, -0.273, -0.259, + -0.217, 0.359, -0.207, 0.352, -0.142, 0.192, -0.061, -0.17 , + -0.343, 0.189, -0.221, 0.32 , -0.301, -0.1 , 0.005, 0.232, + -0.344, 0.136, 0.252, 0.157, -0.13 , -0.244, 0.193, -0.034, + -0.12 , -0.193, -0.102, 0.252, -0.185, -0.167, -0.575, 0.582, + -0.426, 0.983, 0.212, 0.204, 0.03 , -0.276, -0.425, -0.158] k: 3 num_candidates: 3 - # Depending on how things are distributed, docs 2 and 3 might be swapped - # here we verify that are last hit is always the worst one - - match: { hits.hits.2._id: "1" } + - match: { hits.hits.0._id: "1" } + - match: { hits.hits.1._id: "3" } + - match: { hits.hits.2._id: "2" } --- "Test bad parameters": - do: diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 331a2bc0ddda..ff902dbede00 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -459,7 +459,9 @@ org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat, org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat, org.elasticsearch.index.codec.vectors.es816.ES816BinaryQuantizedVectorsFormat, - org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat; + org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat, + org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat, + org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat; provides org.apache.lucene.codecs.Codec with diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/BQVectorUtils.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/BQVectorUtils.java index 5201e57179cc..1aff06a17596 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/BQVectorUtils.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/BQVectorUtils.java @@ -40,6 +40,20 @@ public static boolean isUnitVector(float[] v) { return Math.abs(l1norm - 1.0d) <= EPSILON; } + public static void packAsBinary(byte[] vector, byte[] packed) { + for (int i = 0; i < vector.length;) { + byte result = 0; + for (int j = 7; j >= 0 && i < vector.length; j--) { + assert vector[i] == 0 || vector[i] == 1; + result |= (byte) ((vector[i] & 1) << j); + ++i; + } + int index = ((i + 7) / 8) - 1; + assert index < packed.length; + packed[index] = result; + } + } + public static int discretize(int value, int bucket) { return ((value + (bucket - 1)) / bucket) * bucket; } diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorer.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorer.java index 445bdadab235..e85079e998c6 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorer.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorer.java @@ -48,13 +48,8 @@ class ES816BinaryFlatVectorsScorer implements FlatVectorsScorer { public RandomVectorScorerSupplier getRandomVectorScorerSupplier( VectorSimilarityFunction similarityFunction, KnnVectorValues vectorValues - ) throws IOException { - if (vectorValues instanceof BinarizedByteVectorValues) { - throw new UnsupportedOperationException( - "getRandomVectorScorerSupplier(VectorSimilarityFunction,RandomAccessVectorValues) not implemented for binarized format" - ); - } - return nonQuantizedDelegate.getRandomVectorScorerSupplier(similarityFunction, vectorValues); + ) { + throw new UnsupportedOperationException(); } @Override @@ -90,61 +85,11 @@ public RandomVectorScorer getRandomVectorScorer( return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target); } - RandomVectorScorerSupplier getRandomVectorScorerSupplier( - VectorSimilarityFunction similarityFunction, - ES816BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues scoringVectors, - BinarizedByteVectorValues targetVectors - ) { - return new BinarizedRandomVectorScorerSupplier(scoringVectors, targetVectors, similarityFunction); - } - @Override public String toString() { return "ES816BinaryFlatVectorsScorer(nonQuantizedDelegate=" + nonQuantizedDelegate + ")"; } - /** Vector scorer supplier over binarized vector values */ - static class BinarizedRandomVectorScorerSupplier implements RandomVectorScorerSupplier { - private final ES816BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors; - private final BinarizedByteVectorValues targetVectors; - private final VectorSimilarityFunction similarityFunction; - - BinarizedRandomVectorScorerSupplier( - ES816BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors, - BinarizedByteVectorValues targetVectors, - VectorSimilarityFunction similarityFunction - ) { - this.queryVectors = queryVectors; - this.targetVectors = targetVectors; - this.similarityFunction = similarityFunction; - } - - @Override - public RandomVectorScorer scorer(int ord) throws IOException { - byte[] vector = queryVectors.vectorValue(ord); - int quantizedSum = queryVectors.sumQuantizedValues(ord); - float distanceToCentroid = queryVectors.getCentroidDistance(ord); - float lower = queryVectors.getLower(ord); - float width = queryVectors.getWidth(ord); - float normVmC = 0f; - float vDotC = 0f; - if (similarityFunction != EUCLIDEAN) { - normVmC = queryVectors.getNormVmC(ord); - vDotC = queryVectors.getVDotC(ord); - } - BinaryQueryVector binaryQueryVector = new BinaryQueryVector( - vector, - new BinaryQuantizer.QueryFactors(quantizedSum, distanceToCentroid, lower, width, normVmC, vDotC) - ); - return new BinarizedRandomVectorScorer(binaryQueryVector, targetVectors, similarityFunction); - } - - @Override - public RandomVectorScorerSupplier copy() throws IOException { - return new BinarizedRandomVectorScorerSupplier(queryVectors.copy(), targetVectors.copy(), similarityFunction); - } - } - /** A binarized query representing its quantized form along with factors */ record BinaryQueryVector(byte[] vector, BinaryQuantizer.QueryFactors factors) {} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormat.java index d864ec5dee8c..61b6edc474d1 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormat.java @@ -62,7 +62,7 @@ public ES816BinaryQuantizedVectorsFormat() { @Override public FlatVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - return new ES816BinaryQuantizedVectorsWriter(scorer, rawVectorFormat.fieldsWriter(state), state); + throw new UnsupportedOperationException(); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormat.java index 52f9f14b7bf9..1dbb4e432b18 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormat.java +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormat.java @@ -25,10 +25,8 @@ import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; -import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsWriter; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; -import org.apache.lucene.search.TaskExecutor; import org.apache.lucene.util.hnsw.HnswGraph; import java.io.IOException; @@ -52,21 +50,18 @@ public class ES816HnswBinaryQuantizedVectorsFormat extends KnnVectorsFormat { * Controls how many of the nearest neighbor candidates are connected to the new node. Defaults to * {@link Lucene99HnswVectorsFormat#DEFAULT_MAX_CONN}. See {@link HnswGraph} for more details. */ - private final int maxConn; + protected final int maxConn; /** * The number of candidate neighbors to track while searching the graph for each newly inserted * node. Defaults to {@link Lucene99HnswVectorsFormat#DEFAULT_BEAM_WIDTH}. See {@link HnswGraph} * for details. */ - private final int beamWidth; + protected final int beamWidth; /** The format for storing, reading, merging vectors on disk */ private static final FlatVectorsFormat flatVectorsFormat = new ES816BinaryQuantizedVectorsFormat(); - private final int numMergeWorkers; - private final TaskExecutor mergeExec; - /** Constructs a format using default graph construction parameters */ public ES816HnswBinaryQuantizedVectorsFormat() { this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); @@ -109,17 +104,11 @@ public ES816HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int num if (numMergeWorkers == 1 && mergeExec != null) { throw new IllegalArgumentException("No executor service is needed as we'll use single thread to merge"); } - this.numMergeWorkers = numMergeWorkers; - if (mergeExec != null) { - this.mergeExec = new TaskExecutor(mergeExec); - } else { - this.mergeExec = null; - } } @Override public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { - return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), numMergeWorkers, mergeExec); + throw new UnsupportedOperationException(); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/BinarizedByteVectorValues.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/BinarizedByteVectorValues.java new file mode 100644 index 000000000000..cc1f7b85e0f7 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/BinarizedByteVectorValues.java @@ -0,0 +1,87 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.index.ByteVectorValues; +import org.apache.lucene.search.VectorScorer; +import org.apache.lucene.util.VectorUtil; +import org.elasticsearch.index.codec.vectors.BQVectorUtils; + +import java.io.IOException; + +/** + * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 + */ +abstract class BinarizedByteVectorValues extends ByteVectorValues { + + /** + * Retrieve the corrective terms for the given vector ordinal. For the dot-product family of + * distances, the corrective terms are, in order + * + *

    + *
  • the lower optimized interval + *
  • the upper optimized interval + *
  • the dot-product of the non-centered vector with the centroid + *
  • the sum of quantized components + *
+ * + * For euclidean: + * + *
    + *
  • the lower optimized interval + *
  • the upper optimized interval + *
  • the l2norm of the centered vector + *
  • the sum of quantized components + *
+ * + * @param vectorOrd the vector ordinal + * @return the corrective terms + * @throws IOException if an I/O error occurs + */ + public abstract OptimizedScalarQuantizer.QuantizationResult getCorrectiveTerms(int vectorOrd) throws IOException; + + /** + * @return the quantizer used to quantize the vectors + */ + public abstract OptimizedScalarQuantizer getQuantizer(); + + public abstract float[] getCentroid() throws IOException; + + int discretizedDimensions() { + return BQVectorUtils.discretize(dimension(), 64); + } + + /** + * Return a {@link VectorScorer} for the given query vector. + * + * @param query the query vector + * @return a {@link VectorScorer} instance or null + */ + public abstract VectorScorer scorer(float[] query) throws IOException; + + @Override + public abstract BinarizedByteVectorValues copy() throws IOException; + + float getCentroidDP() throws IOException { + // this only gets executed on-merge + float[] centroid = getCentroid(); + return VectorUtil.dotProduct(centroid, centroid); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryFlatVectorsScorer.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryFlatVectorsScorer.java new file mode 100644 index 000000000000..7c7e470909eb --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryFlatVectorsScorer.java @@ -0,0 +1,188 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; +import org.apache.lucene.index.KnnVectorValues; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.VectorUtil; +import org.apache.lucene.util.hnsw.RandomVectorScorer; +import org.apache.lucene.util.hnsw.RandomVectorScorerSupplier; +import org.elasticsearch.index.codec.vectors.BQSpaceUtils; +import org.elasticsearch.simdvec.ESVectorUtil; + +import java.io.IOException; + +import static org.apache.lucene.index.VectorSimilarityFunction.COSINE; +import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; +import static org.apache.lucene.index.VectorSimilarityFunction.MAXIMUM_INNER_PRODUCT; + +/** Vector scorer over binarized vector values */ +public class ES818BinaryFlatVectorsScorer implements FlatVectorsScorer { + private final FlatVectorsScorer nonQuantizedDelegate; + private static final float FOUR_BIT_SCALE = 1f / ((1 << 4) - 1); + + public ES818BinaryFlatVectorsScorer(FlatVectorsScorer nonQuantizedDelegate) { + this.nonQuantizedDelegate = nonQuantizedDelegate; + } + + @Override + public RandomVectorScorerSupplier getRandomVectorScorerSupplier( + VectorSimilarityFunction similarityFunction, + KnnVectorValues vectorValues + ) throws IOException { + if (vectorValues instanceof BinarizedByteVectorValues) { + throw new UnsupportedOperationException( + "getRandomVectorScorerSupplier(VectorSimilarityFunction,RandomAccessVectorValues) not implemented for binarized format" + ); + } + return nonQuantizedDelegate.getRandomVectorScorerSupplier(similarityFunction, vectorValues); + } + + @Override + public RandomVectorScorer getRandomVectorScorer( + VectorSimilarityFunction similarityFunction, + KnnVectorValues vectorValues, + float[] target + ) throws IOException { + if (vectorValues instanceof BinarizedByteVectorValues binarizedVectors) { + OptimizedScalarQuantizer quantizer = binarizedVectors.getQuantizer(); + float[] centroid = binarizedVectors.getCentroid(); + // We make a copy as the quantization process mutates the input + float[] copy = ArrayUtil.copyOfSubArray(target, 0, target.length); + if (similarityFunction == COSINE) { + VectorUtil.l2normalize(copy); + } + target = copy; + byte[] initial = new byte[target.length]; + byte[] quantized = new byte[BQSpaceUtils.B_QUERY * binarizedVectors.discretizedDimensions() / 8]; + OptimizedScalarQuantizer.QuantizationResult queryCorrections = quantizer.scalarQuantize(target, initial, (byte) 4, centroid); + BQSpaceUtils.transposeHalfByte(initial, quantized); + BinaryQueryVector queryVector = new BinaryQueryVector(quantized, queryCorrections); + return new BinarizedRandomVectorScorer(queryVector, binarizedVectors, similarityFunction); + } + return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target); + } + + @Override + public RandomVectorScorer getRandomVectorScorer( + VectorSimilarityFunction similarityFunction, + KnnVectorValues vectorValues, + byte[] target + ) throws IOException { + return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target); + } + + RandomVectorScorerSupplier getRandomVectorScorerSupplier( + VectorSimilarityFunction similarityFunction, + ES818BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues scoringVectors, + BinarizedByteVectorValues targetVectors + ) { + return new BinarizedRandomVectorScorerSupplier(scoringVectors, targetVectors, similarityFunction); + } + + @Override + public String toString() { + return "ES818BinaryFlatVectorsScorer(nonQuantizedDelegate=" + nonQuantizedDelegate + ")"; + } + + /** Vector scorer supplier over binarized vector values */ + static class BinarizedRandomVectorScorerSupplier implements RandomVectorScorerSupplier { + private final ES818BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors; + private final BinarizedByteVectorValues targetVectors; + private final VectorSimilarityFunction similarityFunction; + + BinarizedRandomVectorScorerSupplier( + ES818BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors, + BinarizedByteVectorValues targetVectors, + VectorSimilarityFunction similarityFunction + ) { + this.queryVectors = queryVectors; + this.targetVectors = targetVectors; + this.similarityFunction = similarityFunction; + } + + @Override + public RandomVectorScorer scorer(int ord) throws IOException { + byte[] vector = queryVectors.vectorValue(ord); + OptimizedScalarQuantizer.QuantizationResult correctiveTerms = queryVectors.getCorrectiveTerms(ord); + BinaryQueryVector binaryQueryVector = new BinaryQueryVector(vector, correctiveTerms); + return new BinarizedRandomVectorScorer(binaryQueryVector, targetVectors, similarityFunction); + } + + @Override + public RandomVectorScorerSupplier copy() throws IOException { + return new BinarizedRandomVectorScorerSupplier(queryVectors.copy(), targetVectors.copy(), similarityFunction); + } + } + + /** A binarized query representing its quantized form along with factors */ + public record BinaryQueryVector(byte[] vector, OptimizedScalarQuantizer.QuantizationResult quantizationResult) {} + + /** Vector scorer over binarized vector values */ + public static class BinarizedRandomVectorScorer extends RandomVectorScorer.AbstractRandomVectorScorer { + private final BinaryQueryVector queryVector; + private final BinarizedByteVectorValues targetVectors; + private final VectorSimilarityFunction similarityFunction; + + public BinarizedRandomVectorScorer( + BinaryQueryVector queryVectors, + BinarizedByteVectorValues targetVectors, + VectorSimilarityFunction similarityFunction + ) { + super(targetVectors); + this.queryVector = queryVectors; + this.targetVectors = targetVectors; + this.similarityFunction = similarityFunction; + } + + @Override + public float score(int targetOrd) throws IOException { + byte[] quantizedQuery = queryVector.vector(); + byte[] binaryCode = targetVectors.vectorValue(targetOrd); + float qcDist = ESVectorUtil.ipByteBinByte(quantizedQuery, binaryCode); + OptimizedScalarQuantizer.QuantizationResult queryCorrections = queryVector.quantizationResult(); + OptimizedScalarQuantizer.QuantizationResult indexCorrections = targetVectors.getCorrectiveTerms(targetOrd); + float x1 = indexCorrections.quantizedComponentSum(); + float ax = indexCorrections.lowerInterval(); + // Here we assume `lx` is simply bit vectors, so the scaling isn't necessary + float lx = indexCorrections.upperInterval() - ax; + float ay = queryCorrections.lowerInterval(); + float ly = (queryCorrections.upperInterval() - ay) * FOUR_BIT_SCALE; + float y1 = queryCorrections.quantizedComponentSum(); + float score = ax * ay * targetVectors.dimension() + ay * lx * x1 + ax * ly * y1 + lx * ly * qcDist; + // For euclidean, we need to invert the score and apply the additional correction, which is + // assumed to be the squared l2norm of the centroid centered vectors. + if (similarityFunction == EUCLIDEAN) { + score = queryCorrections.additionalCorrection() + indexCorrections.additionalCorrection() - 2 * score; + return Math.max(1 / (1f + score), 0); + } else { + // For cosine and max inner product, we need to apply the additional correction, which is + // assumed to be the non-centered dot-product between the vector and the centroid + score += queryCorrections.additionalCorrection() + indexCorrections.additionalCorrection() - targetVectors.getCentroidDP(); + if (similarityFunction == MAXIMUM_INNER_PRODUCT) { + return VectorUtil.scaleMaxInnerProductScore(score); + } + return Math.max((1f + score) / 2f, 0); + } + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java new file mode 100644 index 000000000000..1dee9599f985 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormat.java @@ -0,0 +1,132 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsReader; +import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.index.SegmentWriteState; + +import java.io.IOException; + +import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.MAX_DIMS_COUNT; + +/** + * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 + * Codec for encoding/decoding binary quantized vectors The binary quantization format used here + * is a per-vector optimized scalar quantization. Also see {@link + * org.elasticsearch.index.codec.vectors.es818.OptimizedScalarQuantizer}. Some of key features are: + * + *
    + *
  • Estimating the distance between two vectors using their centroid normalized distance. This + * requires some additional corrective factors, but allows for centroid normalization to occur. + *
  • Optimized scalar quantization to bit level of centroid normalized vectors. + *
  • Asymmetric quantization of vectors, where query vectors are quantized to half-byte + * precision (normalized to the centroid) and then compared directly against the single bit + * quantized vectors in the index. + *
  • Transforming the half-byte quantized query vectors in such a way that the comparison with + * single bit vectors can be done with bit arithmetic. + *
+ * + * The format is stored in two files: + * + *

.veb (vector data) file

+ * + *

Stores the binary quantized vectors in a flat format. Additionally, it stores each vector's + * corrective factors. At the end of the file, additional information is stored for vector ordinal + * to centroid ordinal mapping and sparse vector information. + * + *

    + *
  • For each vector: + *
      + *
    • [byte] the binary quantized values, each byte holds 8 bits. + *
    • [float] the optimized quantiles and an additional similarity dependent corrective factor. + *
    • short the sum of the quantized components
    • + *
    + *
  • After the vectors, sparse vector information keeping track of monotonic blocks. + *
+ * + *

.vemb (vector metadata) file

+ * + *

Stores the metadata for the vectors. This includes the number of vectors, the number of + * dimensions, and file offset information. + * + *

    + *
  • int the field number + *
  • int the vector encoding ordinal + *
  • int the vector similarity ordinal + *
  • vint the vector dimensions + *
  • vlong the offset to the vector data in the .veb file + *
  • vlong the length of the vector data in the .veb file + *
  • vint the number of vectors + *
  • [float] the centroid
  • + *
  • float the centroid square magnitude
  • + *
  • The sparse vector information, if required, mapping vector ordinal to doc ID + *
+ */ +public class ES818BinaryQuantizedVectorsFormat extends FlatVectorsFormat { + + public static final String BINARIZED_VECTOR_COMPONENT = "BVEC"; + public static final String NAME = "ES818BinaryQuantizedVectorsFormat"; + + static final int VERSION_START = 0; + static final int VERSION_CURRENT = VERSION_START; + static final String META_CODEC_NAME = "ES818BinaryQuantizedVectorsFormatMeta"; + static final String VECTOR_DATA_CODEC_NAME = "ES818BinaryQuantizedVectorsFormatData"; + static final String META_EXTENSION = "vemb"; + static final String VECTOR_DATA_EXTENSION = "veb"; + static final int DIRECT_MONOTONIC_BLOCK_SHIFT = 16; + + private static final FlatVectorsFormat rawVectorFormat = new Lucene99FlatVectorsFormat( + FlatVectorScorerUtil.getLucene99FlatVectorsScorer() + ); + + private static final ES818BinaryFlatVectorsScorer scorer = new ES818BinaryFlatVectorsScorer( + FlatVectorScorerUtil.getLucene99FlatVectorsScorer() + ); + + /** Creates a new instance with the default number of vectors per cluster. */ + public ES818BinaryQuantizedVectorsFormat() { + super(NAME); + } + + @Override + public FlatVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + return new ES818BinaryQuantizedVectorsWriter(scorer, rawVectorFormat.fieldsWriter(state), state); + } + + @Override + public FlatVectorsReader fieldsReader(SegmentReadState state) throws IOException { + return new ES818BinaryQuantizedVectorsReader(state, rawVectorFormat.fieldsReader(state), scorer); + } + + @Override + public int getMaxDimensions(String fieldName) { + return MAX_DIMS_COUNT; + } + + @Override + public String toString() { + return "ES818BinaryQuantizedVectorsFormat(name=" + NAME + ", flatVectorScorer=" + scorer + ")"; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsReader.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsReader.java new file mode 100644 index 000000000000..8036b8314cdc --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsReader.java @@ -0,0 +1,412 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.codecs.hnsw.FlatVectorsReader; +import org.apache.lucene.codecs.lucene95.OrdToDocDISIReaderConfiguration; +import org.apache.lucene.index.ByteVectorValues; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.IndexFileNames; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.index.VectorEncoding; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.search.KnnCollector; +import org.apache.lucene.search.VectorScorer; +import org.apache.lucene.store.ChecksumIndexInput; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.ReadAdvice; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.RamUsageEstimator; +import org.apache.lucene.util.SuppressForbidden; +import org.apache.lucene.util.hnsw.OrdinalTranslatedKnnCollector; +import org.apache.lucene.util.hnsw.RandomVectorScorer; +import org.elasticsearch.index.codec.vectors.BQVectorUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader.readSimilarityFunction; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader.readVectorEncoding; + +/** + * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 + */ +@SuppressForbidden(reason = "Lucene classes") +class ES818BinaryQuantizedVectorsReader extends FlatVectorsReader { + + private static final long SHALLOW_SIZE = RamUsageEstimator.shallowSizeOfInstance(ES818BinaryQuantizedVectorsReader.class); + + private final Map fields = new HashMap<>(); + private final IndexInput quantizedVectorData; + private final FlatVectorsReader rawVectorsReader; + private final ES818BinaryFlatVectorsScorer vectorScorer; + + ES818BinaryQuantizedVectorsReader( + SegmentReadState state, + FlatVectorsReader rawVectorsReader, + ES818BinaryFlatVectorsScorer vectorsScorer + ) throws IOException { + super(vectorsScorer); + this.vectorScorer = vectorsScorer; + this.rawVectorsReader = rawVectorsReader; + int versionMeta = -1; + String metaFileName = IndexFileNames.segmentFileName( + state.segmentInfo.name, + state.segmentSuffix, + org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat.META_EXTENSION + ); + boolean success = false; + try (ChecksumIndexInput meta = state.directory.openChecksumInput(metaFileName)) { + Throwable priorE = null; + try { + versionMeta = CodecUtil.checkIndexHeader( + meta, + ES818BinaryQuantizedVectorsFormat.META_CODEC_NAME, + ES818BinaryQuantizedVectorsFormat.VERSION_START, + ES818BinaryQuantizedVectorsFormat.VERSION_CURRENT, + state.segmentInfo.getId(), + state.segmentSuffix + ); + readFields(meta, state.fieldInfos); + } catch (Throwable exception) { + priorE = exception; + } finally { + CodecUtil.checkFooter(meta, priorE); + } + quantizedVectorData = openDataInput( + state, + versionMeta, + ES818BinaryQuantizedVectorsFormat.VECTOR_DATA_EXTENSION, + ES818BinaryQuantizedVectorsFormat.VECTOR_DATA_CODEC_NAME, + // Quantized vectors are accessed randomly from their node ID stored in the HNSW + // graph. + state.context.withReadAdvice(ReadAdvice.RANDOM) + ); + success = true; + } finally { + if (success == false) { + IOUtils.closeWhileHandlingException(this); + } + } + } + + private void readFields(ChecksumIndexInput meta, FieldInfos infos) throws IOException { + for (int fieldNumber = meta.readInt(); fieldNumber != -1; fieldNumber = meta.readInt()) { + FieldInfo info = infos.fieldInfo(fieldNumber); + if (info == null) { + throw new CorruptIndexException("Invalid field number: " + fieldNumber, meta); + } + FieldEntry fieldEntry = readField(meta, info); + validateFieldEntry(info, fieldEntry); + fields.put(info.name, fieldEntry); + } + } + + static void validateFieldEntry(FieldInfo info, FieldEntry fieldEntry) { + int dimension = info.getVectorDimension(); + if (dimension != fieldEntry.dimension) { + throw new IllegalStateException( + "Inconsistent vector dimension for field=\"" + info.name + "\"; " + dimension + " != " + fieldEntry.dimension + ); + } + + int binaryDims = BQVectorUtils.discretize(dimension, 64) / 8; + long numQuantizedVectorBytes = Math.multiplyExact((binaryDims + (Float.BYTES * 3) + Short.BYTES), (long) fieldEntry.size); + if (numQuantizedVectorBytes != fieldEntry.vectorDataLength) { + throw new IllegalStateException( + "Binarized vector data length " + + fieldEntry.vectorDataLength + + " not matching size = " + + fieldEntry.size + + " * (binaryBytes=" + + binaryDims + + " + 14" + + ") = " + + numQuantizedVectorBytes + ); + } + } + + @Override + public RandomVectorScorer getRandomVectorScorer(String field, float[] target) throws IOException { + FieldEntry fi = fields.get(field); + if (fi == null) { + return null; + } + return vectorScorer.getRandomVectorScorer( + fi.similarityFunction, + OffHeapBinarizedVectorValues.load( + fi.ordToDocDISIReaderConfiguration, + fi.dimension, + fi.size, + new OptimizedScalarQuantizer(fi.similarityFunction), + fi.similarityFunction, + vectorScorer, + fi.centroid, + fi.centroidDP, + fi.vectorDataOffset, + fi.vectorDataLength, + quantizedVectorData + ), + target + ); + } + + @Override + public RandomVectorScorer getRandomVectorScorer(String field, byte[] target) throws IOException { + return rawVectorsReader.getRandomVectorScorer(field, target); + } + + @Override + public void checkIntegrity() throws IOException { + rawVectorsReader.checkIntegrity(); + CodecUtil.checksumEntireFile(quantizedVectorData); + } + + @Override + public FloatVectorValues getFloatVectorValues(String field) throws IOException { + FieldEntry fi = fields.get(field); + if (fi == null) { + return null; + } + if (fi.vectorEncoding != VectorEncoding.FLOAT32) { + throw new IllegalArgumentException( + "field=\"" + field + "\" is encoded as: " + fi.vectorEncoding + " expected: " + VectorEncoding.FLOAT32 + ); + } + OffHeapBinarizedVectorValues bvv = OffHeapBinarizedVectorValues.load( + fi.ordToDocDISIReaderConfiguration, + fi.dimension, + fi.size, + new OptimizedScalarQuantizer(fi.similarityFunction), + fi.similarityFunction, + vectorScorer, + fi.centroid, + fi.centroidDP, + fi.vectorDataOffset, + fi.vectorDataLength, + quantizedVectorData + ); + return new BinarizedVectorValues(rawVectorsReader.getFloatVectorValues(field), bvv); + } + + @Override + public ByteVectorValues getByteVectorValues(String field) throws IOException { + return rawVectorsReader.getByteVectorValues(field); + } + + @Override + public void search(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException { + rawVectorsReader.search(field, target, knnCollector, acceptDocs); + } + + @Override + public void search(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException { + if (knnCollector.k() == 0) return; + final RandomVectorScorer scorer = getRandomVectorScorer(field, target); + if (scorer == null) return; + OrdinalTranslatedKnnCollector collector = new OrdinalTranslatedKnnCollector(knnCollector, scorer::ordToDoc); + Bits acceptedOrds = scorer.getAcceptOrds(acceptDocs); + for (int i = 0; i < scorer.maxOrd(); i++) { + if (acceptedOrds == null || acceptedOrds.get(i)) { + collector.collect(i, scorer.score(i)); + collector.incVisitedCount(1); + } + } + } + + @Override + public void close() throws IOException { + IOUtils.close(quantizedVectorData, rawVectorsReader); + } + + @Override + public long ramBytesUsed() { + long size = SHALLOW_SIZE; + size += RamUsageEstimator.sizeOfMap(fields, RamUsageEstimator.shallowSizeOfInstance(FieldEntry.class)); + size += rawVectorsReader.ramBytesUsed(); + return size; + } + + public float[] getCentroid(String field) { + FieldEntry fieldEntry = fields.get(field); + if (fieldEntry != null) { + return fieldEntry.centroid; + } + return null; + } + + private static IndexInput openDataInput( + SegmentReadState state, + int versionMeta, + String fileExtension, + String codecName, + IOContext context + ) throws IOException { + String fileName = IndexFileNames.segmentFileName(state.segmentInfo.name, state.segmentSuffix, fileExtension); + IndexInput in = state.directory.openInput(fileName, context); + boolean success = false; + try { + int versionVectorData = CodecUtil.checkIndexHeader( + in, + codecName, + ES818BinaryQuantizedVectorsFormat.VERSION_START, + ES818BinaryQuantizedVectorsFormat.VERSION_CURRENT, + state.segmentInfo.getId(), + state.segmentSuffix + ); + if (versionMeta != versionVectorData) { + throw new CorruptIndexException( + "Format versions mismatch: meta=" + versionMeta + ", " + codecName + "=" + versionVectorData, + in + ); + } + CodecUtil.retrieveChecksum(in); + success = true; + return in; + } finally { + if (success == false) { + IOUtils.closeWhileHandlingException(in); + } + } + } + + private FieldEntry readField(IndexInput input, FieldInfo info) throws IOException { + VectorEncoding vectorEncoding = readVectorEncoding(input); + VectorSimilarityFunction similarityFunction = readSimilarityFunction(input); + if (similarityFunction != info.getVectorSimilarityFunction()) { + throw new IllegalStateException( + "Inconsistent vector similarity function for field=\"" + + info.name + + "\"; " + + similarityFunction + + " != " + + info.getVectorSimilarityFunction() + ); + } + return FieldEntry.create(input, vectorEncoding, info.getVectorSimilarityFunction()); + } + + private record FieldEntry( + VectorSimilarityFunction similarityFunction, + VectorEncoding vectorEncoding, + int dimension, + int descritizedDimension, + long vectorDataOffset, + long vectorDataLength, + int size, + float[] centroid, + float centroidDP, + OrdToDocDISIReaderConfiguration ordToDocDISIReaderConfiguration + ) { + + static FieldEntry create(IndexInput input, VectorEncoding vectorEncoding, VectorSimilarityFunction similarityFunction) + throws IOException { + int dimension = input.readVInt(); + long vectorDataOffset = input.readVLong(); + long vectorDataLength = input.readVLong(); + int size = input.readVInt(); + final float[] centroid; + float centroidDP = 0; + if (size > 0) { + centroid = new float[dimension]; + input.readFloats(centroid, 0, dimension); + centroidDP = Float.intBitsToFloat(input.readInt()); + } else { + centroid = null; + } + OrdToDocDISIReaderConfiguration conf = OrdToDocDISIReaderConfiguration.fromStoredMeta(input, size); + return new FieldEntry( + similarityFunction, + vectorEncoding, + dimension, + BQVectorUtils.discretize(dimension, 64), + vectorDataOffset, + vectorDataLength, + size, + centroid, + centroidDP, + conf + ); + } + } + + /** Binarized vector values holding row and quantized vector values */ + protected static final class BinarizedVectorValues extends FloatVectorValues { + private final FloatVectorValues rawVectorValues; + private final BinarizedByteVectorValues quantizedVectorValues; + + BinarizedVectorValues(FloatVectorValues rawVectorValues, BinarizedByteVectorValues quantizedVectorValues) { + this.rawVectorValues = rawVectorValues; + this.quantizedVectorValues = quantizedVectorValues; + } + + @Override + public int dimension() { + return rawVectorValues.dimension(); + } + + @Override + public int size() { + return rawVectorValues.size(); + } + + @Override + public float[] vectorValue(int ord) throws IOException { + return rawVectorValues.vectorValue(ord); + } + + @Override + public BinarizedVectorValues copy() throws IOException { + return new BinarizedVectorValues(rawVectorValues.copy(), quantizedVectorValues.copy()); + } + + @Override + public Bits getAcceptOrds(Bits acceptDocs) { + return rawVectorValues.getAcceptOrds(acceptDocs); + } + + @Override + public int ordToDoc(int ord) { + return rawVectorValues.ordToDoc(ord); + } + + @Override + public DocIndexIterator iterator() { + return rawVectorValues.iterator(); + } + + @Override + public VectorScorer scorer(float[] query) throws IOException { + return quantizedVectorValues.scorer(query); + } + + BinarizedByteVectorValues getQuantizedVectorValues() throws IOException { + return quantizedVectorValues; + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsWriter.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsWriter.java new file mode 100644 index 000000000000..02dda6a4a9da --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsWriter.java @@ -0,0 +1,944 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatFieldVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.codecs.lucene95.OrdToDocDISIReaderConfiguration; +import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat; +import org.apache.lucene.index.DocsWithFieldSet; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.IndexFileNames; +import org.apache.lucene.index.KnnVectorValues; +import org.apache.lucene.index.MergeState; +import org.apache.lucene.index.SegmentWriteState; +import org.apache.lucene.index.Sorter; +import org.apache.lucene.index.VectorEncoding; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.internal.hppc.FloatArrayList; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.VectorScorer; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.VectorUtil; +import org.apache.lucene.util.hnsw.CloseableRandomVectorScorerSupplier; +import org.apache.lucene.util.hnsw.RandomVectorScorer; +import org.apache.lucene.util.hnsw.RandomVectorScorerSupplier; +import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.index.codec.vectors.BQSpaceUtils; +import org.elasticsearch.index.codec.vectors.BQVectorUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.apache.lucene.index.VectorSimilarityFunction.COSINE; +import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; +import static org.apache.lucene.util.RamUsageEstimator.shallowSizeOfInstance; +import static org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat.BINARIZED_VECTOR_COMPONENT; +import static org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat.DIRECT_MONOTONIC_BLOCK_SHIFT; + +/** + * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 + */ +@SuppressForbidden(reason = "Lucene classes") +public class ES818BinaryQuantizedVectorsWriter extends FlatVectorsWriter { + private static final long SHALLOW_RAM_BYTES_USED = shallowSizeOfInstance(ES818BinaryQuantizedVectorsWriter.class); + + private final SegmentWriteState segmentWriteState; + private final List fields = new ArrayList<>(); + private final IndexOutput meta, binarizedVectorData; + private final FlatVectorsWriter rawVectorDelegate; + private final ES818BinaryFlatVectorsScorer vectorsScorer; + private boolean finished; + + /** + * Sole constructor + * + * @param vectorsScorer the scorer to use for scoring vectors + */ + protected ES818BinaryQuantizedVectorsWriter( + ES818BinaryFlatVectorsScorer vectorsScorer, + FlatVectorsWriter rawVectorDelegate, + SegmentWriteState state + ) throws IOException { + super(vectorsScorer); + this.vectorsScorer = vectorsScorer; + this.segmentWriteState = state; + String metaFileName = IndexFileNames.segmentFileName( + state.segmentInfo.name, + state.segmentSuffix, + ES818BinaryQuantizedVectorsFormat.META_EXTENSION + ); + + String binarizedVectorDataFileName = IndexFileNames.segmentFileName( + state.segmentInfo.name, + state.segmentSuffix, + ES818BinaryQuantizedVectorsFormat.VECTOR_DATA_EXTENSION + ); + this.rawVectorDelegate = rawVectorDelegate; + boolean success = false; + try { + meta = state.directory.createOutput(metaFileName, state.context); + binarizedVectorData = state.directory.createOutput(binarizedVectorDataFileName, state.context); + + CodecUtil.writeIndexHeader( + meta, + ES818BinaryQuantizedVectorsFormat.META_CODEC_NAME, + ES818BinaryQuantizedVectorsFormat.VERSION_CURRENT, + state.segmentInfo.getId(), + state.segmentSuffix + ); + CodecUtil.writeIndexHeader( + binarizedVectorData, + ES818BinaryQuantizedVectorsFormat.VECTOR_DATA_CODEC_NAME, + ES818BinaryQuantizedVectorsFormat.VERSION_CURRENT, + state.segmentInfo.getId(), + state.segmentSuffix + ); + success = true; + } finally { + if (success == false) { + IOUtils.closeWhileHandlingException(this); + } + } + } + + @Override + public FlatFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException { + FlatFieldVectorsWriter rawVectorDelegate = this.rawVectorDelegate.addField(fieldInfo); + if (fieldInfo.getVectorEncoding().equals(VectorEncoding.FLOAT32)) { + @SuppressWarnings("unchecked") + FieldWriter fieldWriter = new FieldWriter(fieldInfo, (FlatFieldVectorsWriter) rawVectorDelegate); + fields.add(fieldWriter); + return fieldWriter; + } + return rawVectorDelegate; + } + + @Override + public void flush(int maxDoc, Sorter.DocMap sortMap) throws IOException { + rawVectorDelegate.flush(maxDoc, sortMap); + for (FieldWriter field : fields) { + // after raw vectors are written, normalize vectors for clustering and quantization + if (VectorSimilarityFunction.COSINE == field.fieldInfo.getVectorSimilarityFunction()) { + field.normalizeVectors(); + } + final float[] clusterCenter; + int vectorCount = field.flatFieldVectorsWriter.getVectors().size(); + clusterCenter = new float[field.dimensionSums.length]; + if (vectorCount > 0) { + for (int i = 0; i < field.dimensionSums.length; i++) { + clusterCenter[i] = field.dimensionSums[i] / vectorCount; + } + if (VectorSimilarityFunction.COSINE == field.fieldInfo.getVectorSimilarityFunction()) { + VectorUtil.l2normalize(clusterCenter); + } + } + if (segmentWriteState.infoStream.isEnabled(BINARIZED_VECTOR_COMPONENT)) { + segmentWriteState.infoStream.message(BINARIZED_VECTOR_COMPONENT, "Vectors' count:" + vectorCount); + } + OptimizedScalarQuantizer quantizer = new OptimizedScalarQuantizer(field.fieldInfo.getVectorSimilarityFunction()); + if (sortMap == null) { + writeField(field, clusterCenter, maxDoc, quantizer); + } else { + writeSortingField(field, clusterCenter, maxDoc, sortMap, quantizer); + } + field.finish(); + } + } + + private void writeField(FieldWriter fieldData, float[] clusterCenter, int maxDoc, OptimizedScalarQuantizer quantizer) + throws IOException { + // write vector values + long vectorDataOffset = binarizedVectorData.alignFilePointer(Float.BYTES); + writeBinarizedVectors(fieldData, clusterCenter, quantizer); + long vectorDataLength = binarizedVectorData.getFilePointer() - vectorDataOffset; + float centroidDp = fieldData.getVectors().size() > 0 ? VectorUtil.dotProduct(clusterCenter, clusterCenter) : 0; + + writeMeta( + fieldData.fieldInfo, + maxDoc, + vectorDataOffset, + vectorDataLength, + clusterCenter, + centroidDp, + fieldData.getDocsWithFieldSet() + ); + } + + private void writeBinarizedVectors(FieldWriter fieldData, float[] clusterCenter, OptimizedScalarQuantizer scalarQuantizer) + throws IOException { + int discreteDims = BQVectorUtils.discretize(fieldData.fieldInfo.getVectorDimension(), 64); + byte[] quantizationScratch = new byte[discreteDims]; + byte[] vector = new byte[discreteDims / 8]; + for (int i = 0; i < fieldData.getVectors().size(); i++) { + float[] v = fieldData.getVectors().get(i); + OptimizedScalarQuantizer.QuantizationResult corrections = scalarQuantizer.scalarQuantize( + v, + quantizationScratch, + (byte) 1, + clusterCenter + ); + BQVectorUtils.packAsBinary(quantizationScratch, vector); + binarizedVectorData.writeBytes(vector, vector.length); + binarizedVectorData.writeInt(Float.floatToIntBits(corrections.lowerInterval())); + binarizedVectorData.writeInt(Float.floatToIntBits(corrections.upperInterval())); + binarizedVectorData.writeInt(Float.floatToIntBits(corrections.additionalCorrection())); + assert corrections.quantizedComponentSum() >= 0 && corrections.quantizedComponentSum() <= 0xffff; + binarizedVectorData.writeShort((short) corrections.quantizedComponentSum()); + } + } + + private void writeSortingField( + FieldWriter fieldData, + float[] clusterCenter, + int maxDoc, + Sorter.DocMap sortMap, + OptimizedScalarQuantizer scalarQuantizer + ) throws IOException { + final int[] ordMap = new int[fieldData.getDocsWithFieldSet().cardinality()]; // new ord to old ord + + DocsWithFieldSet newDocsWithField = new DocsWithFieldSet(); + mapOldOrdToNewOrd(fieldData.getDocsWithFieldSet(), sortMap, null, ordMap, newDocsWithField); + + // write vector values + long vectorDataOffset = binarizedVectorData.alignFilePointer(Float.BYTES); + writeSortedBinarizedVectors(fieldData, clusterCenter, ordMap, scalarQuantizer); + long quantizedVectorLength = binarizedVectorData.getFilePointer() - vectorDataOffset; + + float centroidDp = VectorUtil.dotProduct(clusterCenter, clusterCenter); + writeMeta(fieldData.fieldInfo, maxDoc, vectorDataOffset, quantizedVectorLength, clusterCenter, centroidDp, newDocsWithField); + } + + private void writeSortedBinarizedVectors( + FieldWriter fieldData, + float[] clusterCenter, + int[] ordMap, + OptimizedScalarQuantizer scalarQuantizer + ) throws IOException { + int discreteDims = BQVectorUtils.discretize(fieldData.fieldInfo.getVectorDimension(), 64); + byte[] quantizationScratch = new byte[discreteDims]; + byte[] vector = new byte[discreteDims / 8]; + for (int ordinal : ordMap) { + float[] v = fieldData.getVectors().get(ordinal); + OptimizedScalarQuantizer.QuantizationResult corrections = scalarQuantizer.scalarQuantize( + v, + quantizationScratch, + (byte) 1, + clusterCenter + ); + BQVectorUtils.packAsBinary(quantizationScratch, vector); + binarizedVectorData.writeBytes(vector, vector.length); + binarizedVectorData.writeInt(Float.floatToIntBits(corrections.lowerInterval())); + binarizedVectorData.writeInt(Float.floatToIntBits(corrections.upperInterval())); + binarizedVectorData.writeInt(Float.floatToIntBits(corrections.additionalCorrection())); + assert corrections.quantizedComponentSum() >= 0 && corrections.quantizedComponentSum() <= 0xffff; + binarizedVectorData.writeShort((short) corrections.quantizedComponentSum()); + } + } + + private void writeMeta( + FieldInfo field, + int maxDoc, + long vectorDataOffset, + long vectorDataLength, + float[] clusterCenter, + float centroidDp, + DocsWithFieldSet docsWithField + ) throws IOException { + meta.writeInt(field.number); + meta.writeInt(field.getVectorEncoding().ordinal()); + meta.writeInt(field.getVectorSimilarityFunction().ordinal()); + meta.writeVInt(field.getVectorDimension()); + meta.writeVLong(vectorDataOffset); + meta.writeVLong(vectorDataLength); + int count = docsWithField.cardinality(); + meta.writeVInt(count); + if (count > 0) { + final ByteBuffer buffer = ByteBuffer.allocate(field.getVectorDimension() * Float.BYTES).order(ByteOrder.LITTLE_ENDIAN); + buffer.asFloatBuffer().put(clusterCenter); + meta.writeBytes(buffer.array(), buffer.array().length); + meta.writeInt(Float.floatToIntBits(centroidDp)); + } + OrdToDocDISIReaderConfiguration.writeStoredMeta( + DIRECT_MONOTONIC_BLOCK_SHIFT, + meta, + binarizedVectorData, + count, + maxDoc, + docsWithField + ); + } + + @Override + public void finish() throws IOException { + if (finished) { + throw new IllegalStateException("already finished"); + } + finished = true; + rawVectorDelegate.finish(); + if (meta != null) { + // write end of fields marker + meta.writeInt(-1); + CodecUtil.writeFooter(meta); + } + if (binarizedVectorData != null) { + CodecUtil.writeFooter(binarizedVectorData); + } + } + + @Override + public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + if (fieldInfo.getVectorEncoding().equals(VectorEncoding.FLOAT32)) { + final float[] centroid; + final float[] mergedCentroid = new float[fieldInfo.getVectorDimension()]; + int vectorCount = mergeAndRecalculateCentroids(mergeState, fieldInfo, mergedCentroid); + // Don't need access to the random vectors, we can just use the merged + rawVectorDelegate.mergeOneField(fieldInfo, mergeState); + centroid = mergedCentroid; + if (segmentWriteState.infoStream.isEnabled(BINARIZED_VECTOR_COMPONENT)) { + segmentWriteState.infoStream.message(BINARIZED_VECTOR_COMPONENT, "Vectors' count:" + vectorCount); + } + FloatVectorValues floatVectorValues = KnnVectorsWriter.MergedVectorValues.mergeFloatVectorValues(fieldInfo, mergeState); + if (fieldInfo.getVectorSimilarityFunction() == COSINE) { + floatVectorValues = new NormalizedFloatVectorValues(floatVectorValues); + } + BinarizedFloatVectorValues binarizedVectorValues = new BinarizedFloatVectorValues( + floatVectorValues, + new OptimizedScalarQuantizer(fieldInfo.getVectorSimilarityFunction()), + centroid + ); + long vectorDataOffset = binarizedVectorData.alignFilePointer(Float.BYTES); + DocsWithFieldSet docsWithField = writeBinarizedVectorData(binarizedVectorData, binarizedVectorValues); + long vectorDataLength = binarizedVectorData.getFilePointer() - vectorDataOffset; + float centroidDp = docsWithField.cardinality() > 0 ? VectorUtil.dotProduct(centroid, centroid) : 0; + writeMeta( + fieldInfo, + segmentWriteState.segmentInfo.maxDoc(), + vectorDataOffset, + vectorDataLength, + centroid, + centroidDp, + docsWithField + ); + } else { + rawVectorDelegate.mergeOneField(fieldInfo, mergeState); + } + } + + static DocsWithFieldSet writeBinarizedVectorAndQueryData( + IndexOutput binarizedVectorData, + IndexOutput binarizedQueryData, + FloatVectorValues floatVectorValues, + float[] centroid, + OptimizedScalarQuantizer binaryQuantizer + ) throws IOException { + int discretizedDimension = BQVectorUtils.discretize(floatVectorValues.dimension(), 64); + DocsWithFieldSet docsWithField = new DocsWithFieldSet(); + byte[][] quantizationScratch = new byte[2][floatVectorValues.dimension()]; + byte[] toIndex = new byte[discretizedDimension / 8]; + byte[] toQuery = new byte[(discretizedDimension / 8) * BQSpaceUtils.B_QUERY]; + KnnVectorValues.DocIndexIterator iterator = floatVectorValues.iterator(); + for (int docV = iterator.nextDoc(); docV != NO_MORE_DOCS; docV = iterator.nextDoc()) { + // write index vector + OptimizedScalarQuantizer.QuantizationResult[] r = binaryQuantizer.multiScalarQuantize( + floatVectorValues.vectorValue(iterator.index()), + quantizationScratch, + new byte[] { 1, 4 }, + centroid + ); + // pack and store document bit vector + BQVectorUtils.packAsBinary(quantizationScratch[0], toIndex); + binarizedVectorData.writeBytes(toIndex, toIndex.length); + binarizedVectorData.writeInt(Float.floatToIntBits(r[0].lowerInterval())); + binarizedVectorData.writeInt(Float.floatToIntBits(r[0].upperInterval())); + binarizedVectorData.writeInt(Float.floatToIntBits(r[0].additionalCorrection())); + assert r[0].quantizedComponentSum() >= 0 && r[0].quantizedComponentSum() <= 0xffff; + binarizedVectorData.writeShort((short) r[0].quantizedComponentSum()); + docsWithField.add(docV); + + // pack and store the 4bit query vector + BQSpaceUtils.transposeHalfByte(quantizationScratch[1], toQuery); + binarizedQueryData.writeBytes(toQuery, toQuery.length); + binarizedQueryData.writeInt(Float.floatToIntBits(r[1].lowerInterval())); + binarizedQueryData.writeInt(Float.floatToIntBits(r[1].upperInterval())); + binarizedQueryData.writeInt(Float.floatToIntBits(r[1].additionalCorrection())); + assert r[1].quantizedComponentSum() >= 0 && r[1].quantizedComponentSum() <= 0xffff; + binarizedQueryData.writeShort((short) r[1].quantizedComponentSum()); + } + return docsWithField; + } + + static DocsWithFieldSet writeBinarizedVectorData(IndexOutput output, BinarizedByteVectorValues binarizedByteVectorValues) + throws IOException { + DocsWithFieldSet docsWithField = new DocsWithFieldSet(); + KnnVectorValues.DocIndexIterator iterator = binarizedByteVectorValues.iterator(); + for (int docV = iterator.nextDoc(); docV != NO_MORE_DOCS; docV = iterator.nextDoc()) { + // write vector + byte[] binaryValue = binarizedByteVectorValues.vectorValue(iterator.index()); + output.writeBytes(binaryValue, binaryValue.length); + OptimizedScalarQuantizer.QuantizationResult corrections = binarizedByteVectorValues.getCorrectiveTerms(iterator.index()); + output.writeInt(Float.floatToIntBits(corrections.lowerInterval())); + output.writeInt(Float.floatToIntBits(corrections.upperInterval())); + output.writeInt(Float.floatToIntBits(corrections.additionalCorrection())); + assert corrections.quantizedComponentSum() >= 0 && corrections.quantizedComponentSum() <= 0xffff; + output.writeShort((short) corrections.quantizedComponentSum()); + docsWithField.add(docV); + } + return docsWithField; + } + + @Override + public CloseableRandomVectorScorerSupplier mergeOneFieldToIndex(FieldInfo fieldInfo, MergeState mergeState) throws IOException { + if (fieldInfo.getVectorEncoding().equals(VectorEncoding.FLOAT32)) { + final float[] centroid; + final float cDotC; + final float[] mergedCentroid = new float[fieldInfo.getVectorDimension()]; + int vectorCount = mergeAndRecalculateCentroids(mergeState, fieldInfo, mergedCentroid); + + // Don't need access to the random vectors, we can just use the merged + rawVectorDelegate.mergeOneField(fieldInfo, mergeState); + centroid = mergedCentroid; + cDotC = vectorCount > 0 ? VectorUtil.dotProduct(centroid, centroid) : 0; + if (segmentWriteState.infoStream.isEnabled(BINARIZED_VECTOR_COMPONENT)) { + segmentWriteState.infoStream.message(BINARIZED_VECTOR_COMPONENT, "Vectors' count:" + vectorCount); + } + return mergeOneFieldToIndex(segmentWriteState, fieldInfo, mergeState, centroid, cDotC); + } + return rawVectorDelegate.mergeOneFieldToIndex(fieldInfo, mergeState); + } + + private CloseableRandomVectorScorerSupplier mergeOneFieldToIndex( + SegmentWriteState segmentWriteState, + FieldInfo fieldInfo, + MergeState mergeState, + float[] centroid, + float cDotC + ) throws IOException { + long vectorDataOffset = binarizedVectorData.alignFilePointer(Float.BYTES); + final IndexOutput tempQuantizedVectorData = segmentWriteState.directory.createTempOutput( + binarizedVectorData.getName(), + "temp", + segmentWriteState.context + ); + final IndexOutput tempScoreQuantizedVectorData = segmentWriteState.directory.createTempOutput( + binarizedVectorData.getName(), + "score_temp", + segmentWriteState.context + ); + IndexInput binarizedDataInput = null; + IndexInput binarizedScoreDataInput = null; + boolean success = false; + OptimizedScalarQuantizer quantizer = new OptimizedScalarQuantizer(fieldInfo.getVectorSimilarityFunction()); + try { + FloatVectorValues floatVectorValues = KnnVectorsWriter.MergedVectorValues.mergeFloatVectorValues(fieldInfo, mergeState); + if (fieldInfo.getVectorSimilarityFunction() == COSINE) { + floatVectorValues = new NormalizedFloatVectorValues(floatVectorValues); + } + DocsWithFieldSet docsWithField = writeBinarizedVectorAndQueryData( + tempQuantizedVectorData, + tempScoreQuantizedVectorData, + floatVectorValues, + centroid, + quantizer + ); + CodecUtil.writeFooter(tempQuantizedVectorData); + IOUtils.close(tempQuantizedVectorData); + binarizedDataInput = segmentWriteState.directory.openInput(tempQuantizedVectorData.getName(), segmentWriteState.context); + binarizedVectorData.copyBytes(binarizedDataInput, binarizedDataInput.length() - CodecUtil.footerLength()); + long vectorDataLength = binarizedVectorData.getFilePointer() - vectorDataOffset; + CodecUtil.retrieveChecksum(binarizedDataInput); + CodecUtil.writeFooter(tempScoreQuantizedVectorData); + IOUtils.close(tempScoreQuantizedVectorData); + binarizedScoreDataInput = segmentWriteState.directory.openInput( + tempScoreQuantizedVectorData.getName(), + segmentWriteState.context + ); + writeMeta( + fieldInfo, + segmentWriteState.segmentInfo.maxDoc(), + vectorDataOffset, + vectorDataLength, + centroid, + cDotC, + docsWithField + ); + success = true; + final IndexInput finalBinarizedDataInput = binarizedDataInput; + final IndexInput finalBinarizedScoreDataInput = binarizedScoreDataInput; + OffHeapBinarizedVectorValues vectorValues = new OffHeapBinarizedVectorValues.DenseOffHeapVectorValues( + fieldInfo.getVectorDimension(), + docsWithField.cardinality(), + centroid, + cDotC, + quantizer, + fieldInfo.getVectorSimilarityFunction(), + vectorsScorer, + finalBinarizedDataInput + ); + RandomVectorScorerSupplier scorerSupplier = vectorsScorer.getRandomVectorScorerSupplier( + fieldInfo.getVectorSimilarityFunction(), + new OffHeapBinarizedQueryVectorValues( + finalBinarizedScoreDataInput, + fieldInfo.getVectorDimension(), + docsWithField.cardinality() + ), + vectorValues + ); + return new BinarizedCloseableRandomVectorScorerSupplier(scorerSupplier, vectorValues, () -> { + IOUtils.close(finalBinarizedDataInput, finalBinarizedScoreDataInput); + IOUtils.deleteFilesIgnoringExceptions( + segmentWriteState.directory, + tempQuantizedVectorData.getName(), + tempScoreQuantizedVectorData.getName() + ); + }); + } finally { + if (success == false) { + IOUtils.closeWhileHandlingException( + tempQuantizedVectorData, + tempScoreQuantizedVectorData, + binarizedDataInput, + binarizedScoreDataInput + ); + IOUtils.deleteFilesIgnoringExceptions( + segmentWriteState.directory, + tempQuantizedVectorData.getName(), + tempScoreQuantizedVectorData.getName() + ); + } + } + } + + @Override + public void close() throws IOException { + IOUtils.close(meta, binarizedVectorData, rawVectorDelegate); + } + + static float[] getCentroid(KnnVectorsReader vectorsReader, String fieldName) { + if (vectorsReader instanceof PerFieldKnnVectorsFormat.FieldsReader candidateReader) { + vectorsReader = candidateReader.getFieldReader(fieldName); + } + if (vectorsReader instanceof ES818BinaryQuantizedVectorsReader reader) { + return reader.getCentroid(fieldName); + } + return null; + } + + static int mergeAndRecalculateCentroids(MergeState mergeState, FieldInfo fieldInfo, float[] mergedCentroid) throws IOException { + boolean recalculate = false; + int totalVectorCount = 0; + for (int i = 0; i < mergeState.knnVectorsReaders.length; i++) { + KnnVectorsReader knnVectorsReader = mergeState.knnVectorsReaders[i]; + if (knnVectorsReader == null || knnVectorsReader.getFloatVectorValues(fieldInfo.name) == null) { + continue; + } + float[] centroid = getCentroid(knnVectorsReader, fieldInfo.name); + int vectorCount = knnVectorsReader.getFloatVectorValues(fieldInfo.name).size(); + if (vectorCount == 0) { + continue; + } + totalVectorCount += vectorCount; + // If there aren't centroids, or previously clustered with more than one cluster + // or if there are deleted docs, we must recalculate the centroid + if (centroid == null || mergeState.liveDocs[i] != null) { + recalculate = true; + break; + } + for (int j = 0; j < centroid.length; j++) { + mergedCentroid[j] += centroid[j] * vectorCount; + } + } + if (recalculate) { + return calculateCentroid(mergeState, fieldInfo, mergedCentroid); + } else { + for (int j = 0; j < mergedCentroid.length; j++) { + mergedCentroid[j] = mergedCentroid[j] / totalVectorCount; + } + if (fieldInfo.getVectorSimilarityFunction() == COSINE) { + VectorUtil.l2normalize(mergedCentroid); + } + return totalVectorCount; + } + } + + static int calculateCentroid(MergeState mergeState, FieldInfo fieldInfo, float[] centroid) throws IOException { + assert fieldInfo.getVectorEncoding().equals(VectorEncoding.FLOAT32); + // clear out the centroid + Arrays.fill(centroid, 0); + int count = 0; + for (int i = 0; i < mergeState.knnVectorsReaders.length; i++) { + KnnVectorsReader knnVectorsReader = mergeState.knnVectorsReaders[i]; + if (knnVectorsReader == null) continue; + FloatVectorValues vectorValues = mergeState.knnVectorsReaders[i].getFloatVectorValues(fieldInfo.name); + if (vectorValues == null) { + continue; + } + KnnVectorValues.DocIndexIterator iterator = vectorValues.iterator(); + for (int doc = iterator.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = iterator.nextDoc()) { + ++count; + float[] vector = vectorValues.vectorValue(iterator.index()); + // TODO Panama sum + for (int j = 0; j < vector.length; j++) { + centroid[j] += vector[j]; + } + } + } + if (count == 0) { + return count; + } + // TODO Panama div + for (int i = 0; i < centroid.length; i++) { + centroid[i] /= count; + } + if (fieldInfo.getVectorSimilarityFunction() == COSINE) { + VectorUtil.l2normalize(centroid); + } + return count; + } + + @Override + public long ramBytesUsed() { + long total = SHALLOW_RAM_BYTES_USED; + for (FieldWriter field : fields) { + // the field tracks the delegate field usage + total += field.ramBytesUsed(); + } + return total; + } + + static class FieldWriter extends FlatFieldVectorsWriter { + private static final long SHALLOW_SIZE = shallowSizeOfInstance(FieldWriter.class); + private final FieldInfo fieldInfo; + private boolean finished; + private final FlatFieldVectorsWriter flatFieldVectorsWriter; + private final float[] dimensionSums; + private final FloatArrayList magnitudes = new FloatArrayList(); + + FieldWriter(FieldInfo fieldInfo, FlatFieldVectorsWriter flatFieldVectorsWriter) { + this.fieldInfo = fieldInfo; + this.flatFieldVectorsWriter = flatFieldVectorsWriter; + this.dimensionSums = new float[fieldInfo.getVectorDimension()]; + } + + @Override + public List getVectors() { + return flatFieldVectorsWriter.getVectors(); + } + + public void normalizeVectors() { + for (int i = 0; i < flatFieldVectorsWriter.getVectors().size(); i++) { + float[] vector = flatFieldVectorsWriter.getVectors().get(i); + float magnitude = magnitudes.get(i); + for (int j = 0; j < vector.length; j++) { + vector[j] /= magnitude; + } + } + } + + @Override + public DocsWithFieldSet getDocsWithFieldSet() { + return flatFieldVectorsWriter.getDocsWithFieldSet(); + } + + @Override + public void finish() throws IOException { + if (finished) { + return; + } + assert flatFieldVectorsWriter.isFinished(); + finished = true; + } + + @Override + public boolean isFinished() { + return finished && flatFieldVectorsWriter.isFinished(); + } + + @Override + public void addValue(int docID, float[] vectorValue) throws IOException { + flatFieldVectorsWriter.addValue(docID, vectorValue); + if (fieldInfo.getVectorSimilarityFunction() == COSINE) { + float dp = VectorUtil.dotProduct(vectorValue, vectorValue); + float divisor = (float) Math.sqrt(dp); + magnitudes.add(divisor); + for (int i = 0; i < vectorValue.length; i++) { + dimensionSums[i] += (vectorValue[i] / divisor); + } + } else { + for (int i = 0; i < vectorValue.length; i++) { + dimensionSums[i] += vectorValue[i]; + } + } + } + + @Override + public float[] copyValue(float[] vectorValue) { + throw new UnsupportedOperationException(); + } + + @Override + public long ramBytesUsed() { + long size = SHALLOW_SIZE; + size += flatFieldVectorsWriter.ramBytesUsed(); + size += magnitudes.ramBytesUsed(); + return size; + } + } + + // When accessing vectorValue method, targerOrd here means a row ordinal. + static class OffHeapBinarizedQueryVectorValues { + private final IndexInput slice; + private final int dimension; + private final int size; + protected final byte[] binaryValue; + protected final ByteBuffer byteBuffer; + private final int byteSize; + protected final float[] correctiveValues; + private int lastOrd = -1; + private int quantizedComponentSum; + + OffHeapBinarizedQueryVectorValues(IndexInput data, int dimension, int size) { + this.slice = data; + this.dimension = dimension; + this.size = size; + // 4x the quantized binary dimensions + int binaryDimensions = (BQVectorUtils.discretize(dimension, 64) / 8) * BQSpaceUtils.B_QUERY; + this.byteBuffer = ByteBuffer.allocate(binaryDimensions); + this.binaryValue = byteBuffer.array(); + // + 1 for the quantized sum + this.correctiveValues = new float[3]; + this.byteSize = binaryDimensions + Float.BYTES * 3 + Short.BYTES; + } + + public OptimizedScalarQuantizer.QuantizationResult getCorrectiveTerms(int targetOrd) throws IOException { + if (lastOrd == targetOrd) { + return new OptimizedScalarQuantizer.QuantizationResult( + correctiveValues[0], + correctiveValues[1], + correctiveValues[2], + quantizedComponentSum + ); + } + vectorValue(targetOrd); + return new OptimizedScalarQuantizer.QuantizationResult( + correctiveValues[0], + correctiveValues[1], + correctiveValues[2], + quantizedComponentSum + ); + } + + public int size() { + return size; + } + + public int dimension() { + return dimension; + } + + public OffHeapBinarizedQueryVectorValues copy() throws IOException { + return new OffHeapBinarizedQueryVectorValues(slice.clone(), dimension, size); + } + + public IndexInput getSlice() { + return slice; + } + + public byte[] vectorValue(int targetOrd) throws IOException { + if (lastOrd == targetOrd) { + return binaryValue; + } + slice.seek((long) targetOrd * byteSize); + slice.readBytes(binaryValue, 0, binaryValue.length); + slice.readFloats(correctiveValues, 0, 3); + quantizedComponentSum = Short.toUnsignedInt(slice.readShort()); + lastOrd = targetOrd; + return binaryValue; + } + } + + static class BinarizedFloatVectorValues extends BinarizedByteVectorValues { + private OptimizedScalarQuantizer.QuantizationResult corrections; + private final byte[] binarized; + private final byte[] initQuantized; + private final float[] centroid; + private final FloatVectorValues values; + private final OptimizedScalarQuantizer quantizer; + + private int lastOrd = -1; + + BinarizedFloatVectorValues(FloatVectorValues delegate, OptimizedScalarQuantizer quantizer, float[] centroid) { + this.values = delegate; + this.quantizer = quantizer; + this.binarized = new byte[BQVectorUtils.discretize(delegate.dimension(), 64) / 8]; + this.initQuantized = new byte[delegate.dimension()]; + this.centroid = centroid; + } + + @Override + public OptimizedScalarQuantizer.QuantizationResult getCorrectiveTerms(int ord) { + if (ord != lastOrd) { + throw new IllegalStateException( + "attempt to retrieve corrective terms for different ord " + ord + " than the quantization was done for: " + lastOrd + ); + } + return corrections; + } + + @Override + public byte[] vectorValue(int ord) throws IOException { + if (ord != lastOrd) { + binarize(ord); + lastOrd = ord; + } + return binarized; + } + + @Override + public int dimension() { + return values.dimension(); + } + + @Override + public OptimizedScalarQuantizer getQuantizer() { + throw new UnsupportedOperationException(); + } + + @Override + public float[] getCentroid() throws IOException { + return centroid; + } + + @Override + public int size() { + return values.size(); + } + + @Override + public VectorScorer scorer(float[] target) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public BinarizedByteVectorValues copy() throws IOException { + return new BinarizedFloatVectorValues(values.copy(), quantizer, centroid); + } + + private void binarize(int ord) throws IOException { + corrections = quantizer.scalarQuantize(values.vectorValue(ord), initQuantized, (byte) 1, centroid); + BQVectorUtils.packAsBinary(initQuantized, binarized); + } + + @Override + public DocIndexIterator iterator() { + return values.iterator(); + } + + @Override + public int ordToDoc(int ord) { + return values.ordToDoc(ord); + } + } + + static class BinarizedCloseableRandomVectorScorerSupplier implements CloseableRandomVectorScorerSupplier { + private final RandomVectorScorerSupplier supplier; + private final KnnVectorValues vectorValues; + private final Closeable onClose; + + BinarizedCloseableRandomVectorScorerSupplier(RandomVectorScorerSupplier supplier, KnnVectorValues vectorValues, Closeable onClose) { + this.supplier = supplier; + this.onClose = onClose; + this.vectorValues = vectorValues; + } + + @Override + public RandomVectorScorer scorer(int ord) throws IOException { + return supplier.scorer(ord); + } + + @Override + public RandomVectorScorerSupplier copy() throws IOException { + return supplier.copy(); + } + + @Override + public void close() throws IOException { + onClose.close(); + } + + @Override + public int totalVectorCount() { + return vectorValues.size(); + } + } + + static final class NormalizedFloatVectorValues extends FloatVectorValues { + private final FloatVectorValues values; + private final float[] normalizedVector; + + NormalizedFloatVectorValues(FloatVectorValues values) { + this.values = values; + this.normalizedVector = new float[values.dimension()]; + } + + @Override + public int dimension() { + return values.dimension(); + } + + @Override + public int size() { + return values.size(); + } + + @Override + public int ordToDoc(int ord) { + return values.ordToDoc(ord); + } + + @Override + public float[] vectorValue(int ord) throws IOException { + System.arraycopy(values.vectorValue(ord), 0, normalizedVector, 0, normalizedVector.length); + VectorUtil.l2normalize(normalizedVector); + return normalizedVector; + } + + @Override + public DocIndexIterator iterator() { + return values.iterator(); + } + + @Override + public NormalizedFloatVectorValues copy() throws IOException { + return new NormalizedFloatVectorValues(values.copy()); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java new file mode 100644 index 000000000000..56942017c3ce --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormat.java @@ -0,0 +1,145 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsWriter; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.index.SegmentWriteState; +import org.apache.lucene.search.TaskExecutor; +import org.apache.lucene.util.hnsw.HnswGraph; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_NUM_MERGE_WORKER; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.MAXIMUM_BEAM_WIDTH; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.MAXIMUM_MAX_CONN; +import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.MAX_DIMS_COUNT; + +/** + * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 + */ +public class ES818HnswBinaryQuantizedVectorsFormat extends KnnVectorsFormat { + + public static final String NAME = "ES818HnswBinaryQuantizedVectorsFormat"; + + /** + * Controls how many of the nearest neighbor candidates are connected to the new node. Defaults to + * {@link Lucene99HnswVectorsFormat#DEFAULT_MAX_CONN}. See {@link HnswGraph} for more details. + */ + private final int maxConn; + + /** + * The number of candidate neighbors to track while searching the graph for each newly inserted + * node. Defaults to {@link Lucene99HnswVectorsFormat#DEFAULT_BEAM_WIDTH}. See {@link HnswGraph} + * for details. + */ + private final int beamWidth; + + /** The format for storing, reading, merging vectors on disk */ + private static final FlatVectorsFormat flatVectorsFormat = new ES818BinaryQuantizedVectorsFormat(); + + private final int numMergeWorkers; + private final TaskExecutor mergeExec; + + /** Constructs a format using default graph construction parameters */ + public ES818HnswBinaryQuantizedVectorsFormat() { + this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH, DEFAULT_NUM_MERGE_WORKER, null); + } + + /** + * Constructs a format using the given graph construction parameters. + * + * @param maxConn the maximum number of connections to a node in the HNSW graph + * @param beamWidth the size of the queue maintained during graph construction. + */ + public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth) { + this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); + } + + /** + * Constructs a format using the given graph construction parameters and scalar quantization. + * + * @param maxConn the maximum number of connections to a node in the HNSW graph + * @param beamWidth the size of the queue maintained during graph construction. + * @param numMergeWorkers number of workers (threads) that will be used when doing merge. If + * larger than 1, a non-null {@link ExecutorService} must be passed as mergeExec + * @param mergeExec the {@link ExecutorService} that will be used by ALL vector writers that are + * generated by this format to do the merge + */ + public ES818HnswBinaryQuantizedVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { + super(NAME); + if (maxConn <= 0 || maxConn > MAXIMUM_MAX_CONN) { + throw new IllegalArgumentException( + "maxConn must be positive and less than or equal to " + MAXIMUM_MAX_CONN + "; maxConn=" + maxConn + ); + } + if (beamWidth <= 0 || beamWidth > MAXIMUM_BEAM_WIDTH) { + throw new IllegalArgumentException( + "beamWidth must be positive and less than or equal to " + MAXIMUM_BEAM_WIDTH + "; beamWidth=" + beamWidth + ); + } + this.maxConn = maxConn; + this.beamWidth = beamWidth; + if (numMergeWorkers == 1 && mergeExec != null) { + throw new IllegalArgumentException("No executor service is needed as we'll use single thread to merge"); + } + this.numMergeWorkers = numMergeWorkers; + if (mergeExec != null) { + this.mergeExec = new TaskExecutor(mergeExec); + } else { + this.mergeExec = null; + } + } + + @Override + public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), numMergeWorkers, mergeExec); + } + + @Override + public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException { + return new Lucene99HnswVectorsReader(state, flatVectorsFormat.fieldsReader(state)); + } + + @Override + public int getMaxDimensions(String fieldName) { + return MAX_DIMS_COUNT; + } + + @Override + public String toString() { + return "ES818HnswBinaryQuantizedVectorsFormat(name=ES818HnswBinaryQuantizedVectorsFormat, maxConn=" + + maxConn + + ", beamWidth=" + + beamWidth + + ", flatVectorFormat=" + + flatVectorsFormat + + ")"; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OffHeapBinarizedVectorValues.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OffHeapBinarizedVectorValues.java new file mode 100644 index 000000000000..72333169b39b --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OffHeapBinarizedVectorValues.java @@ -0,0 +1,371 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; +import org.apache.lucene.codecs.lucene90.IndexedDISI; +import org.apache.lucene.codecs.lucene95.OrdToDocDISIReaderConfiguration; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.VectorScorer; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.hnsw.RandomVectorScorer; +import org.apache.lucene.util.packed.DirectMonotonicReader; +import org.elasticsearch.index.codec.vectors.BQVectorUtils; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** Binarized vector values loaded from off-heap */ +abstract class OffHeapBinarizedVectorValues extends BinarizedByteVectorValues { + + final int dimension; + final int size; + final int numBytes; + final VectorSimilarityFunction similarityFunction; + final FlatVectorsScorer vectorsScorer; + + final IndexInput slice; + final byte[] binaryValue; + final ByteBuffer byteBuffer; + final int byteSize; + private int lastOrd = -1; + final float[] correctiveValues; + int quantizedComponentSum; + final OptimizedScalarQuantizer binaryQuantizer; + final float[] centroid; + final float centroidDp; + private final int discretizedDimensions; + + OffHeapBinarizedVectorValues( + int dimension, + int size, + float[] centroid, + float centroidDp, + OptimizedScalarQuantizer quantizer, + VectorSimilarityFunction similarityFunction, + FlatVectorsScorer vectorsScorer, + IndexInput slice + ) { + this.dimension = dimension; + this.size = size; + this.similarityFunction = similarityFunction; + this.vectorsScorer = vectorsScorer; + this.slice = slice; + this.centroid = centroid; + this.centroidDp = centroidDp; + this.numBytes = BQVectorUtils.discretize(dimension, 64) / 8; + this.correctiveValues = new float[3]; + this.byteSize = numBytes + (Float.BYTES * 3) + Short.BYTES; + this.byteBuffer = ByteBuffer.allocate(numBytes); + this.binaryValue = byteBuffer.array(); + this.binaryQuantizer = quantizer; + this.discretizedDimensions = BQVectorUtils.discretize(dimension, 64); + } + + @Override + public int dimension() { + return dimension; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] vectorValue(int targetOrd) throws IOException { + if (lastOrd == targetOrd) { + return binaryValue; + } + slice.seek((long) targetOrd * byteSize); + slice.readBytes(byteBuffer.array(), byteBuffer.arrayOffset(), numBytes); + slice.readFloats(correctiveValues, 0, 3); + quantizedComponentSum = Short.toUnsignedInt(slice.readShort()); + lastOrd = targetOrd; + return binaryValue; + } + + @Override + public int discretizedDimensions() { + return discretizedDimensions; + } + + @Override + public float getCentroidDP() { + return centroidDp; + } + + @Override + public OptimizedScalarQuantizer.QuantizationResult getCorrectiveTerms(int targetOrd) throws IOException { + if (lastOrd == targetOrd) { + return new OptimizedScalarQuantizer.QuantizationResult( + correctiveValues[0], + correctiveValues[1], + correctiveValues[2], + quantizedComponentSum + ); + } + slice.seek(((long) targetOrd * byteSize) + numBytes); + slice.readFloats(correctiveValues, 0, 3); + quantizedComponentSum = Short.toUnsignedInt(slice.readShort()); + return new OptimizedScalarQuantizer.QuantizationResult( + correctiveValues[0], + correctiveValues[1], + correctiveValues[2], + quantizedComponentSum + ); + } + + @Override + public OptimizedScalarQuantizer getQuantizer() { + return binaryQuantizer; + } + + @Override + public float[] getCentroid() { + return centroid; + } + + @Override + public int getVectorByteLength() { + return numBytes; + } + + static OffHeapBinarizedVectorValues load( + OrdToDocDISIReaderConfiguration configuration, + int dimension, + int size, + OptimizedScalarQuantizer binaryQuantizer, + VectorSimilarityFunction similarityFunction, + FlatVectorsScorer vectorsScorer, + float[] centroid, + float centroidDp, + long quantizedVectorDataOffset, + long quantizedVectorDataLength, + IndexInput vectorData + ) throws IOException { + if (configuration.isEmpty()) { + return new EmptyOffHeapVectorValues(dimension, similarityFunction, vectorsScorer); + } + assert centroid != null; + IndexInput bytesSlice = vectorData.slice("quantized-vector-data", quantizedVectorDataOffset, quantizedVectorDataLength); + if (configuration.isDense()) { + return new DenseOffHeapVectorValues( + dimension, + size, + centroid, + centroidDp, + binaryQuantizer, + similarityFunction, + vectorsScorer, + bytesSlice + ); + } else { + return new SparseOffHeapVectorValues( + configuration, + dimension, + size, + centroid, + centroidDp, + binaryQuantizer, + vectorData, + similarityFunction, + vectorsScorer, + bytesSlice + ); + } + } + + /** Dense off-heap binarized vector values */ + static class DenseOffHeapVectorValues extends OffHeapBinarizedVectorValues { + DenseOffHeapVectorValues( + int dimension, + int size, + float[] centroid, + float centroidDp, + OptimizedScalarQuantizer binaryQuantizer, + VectorSimilarityFunction similarityFunction, + FlatVectorsScorer vectorsScorer, + IndexInput slice + ) { + super(dimension, size, centroid, centroidDp, binaryQuantizer, similarityFunction, vectorsScorer, slice); + } + + @Override + public DenseOffHeapVectorValues copy() throws IOException { + return new DenseOffHeapVectorValues( + dimension, + size, + centroid, + centroidDp, + binaryQuantizer, + similarityFunction, + vectorsScorer, + slice.clone() + ); + } + + @Override + public Bits getAcceptOrds(Bits acceptDocs) { + return acceptDocs; + } + + @Override + public VectorScorer scorer(float[] target) throws IOException { + DenseOffHeapVectorValues copy = copy(); + DocIndexIterator iterator = copy.iterator(); + RandomVectorScorer scorer = vectorsScorer.getRandomVectorScorer(similarityFunction, copy, target); + return new VectorScorer() { + @Override + public float score() throws IOException { + return scorer.score(iterator.index()); + } + + @Override + public DocIdSetIterator iterator() { + return iterator; + } + }; + } + + @Override + public DocIndexIterator iterator() { + return createDenseIterator(); + } + } + + /** Sparse off-heap binarized vector values */ + private static class SparseOffHeapVectorValues extends OffHeapBinarizedVectorValues { + private final DirectMonotonicReader ordToDoc; + private final IndexedDISI disi; + // dataIn was used to init a new IndexedDIS for #randomAccess() + private final IndexInput dataIn; + private final OrdToDocDISIReaderConfiguration configuration; + + SparseOffHeapVectorValues( + OrdToDocDISIReaderConfiguration configuration, + int dimension, + int size, + float[] centroid, + float centroidDp, + OptimizedScalarQuantizer binaryQuantizer, + IndexInput dataIn, + VectorSimilarityFunction similarityFunction, + FlatVectorsScorer vectorsScorer, + IndexInput slice + ) throws IOException { + super(dimension, size, centroid, centroidDp, binaryQuantizer, similarityFunction, vectorsScorer, slice); + this.configuration = configuration; + this.dataIn = dataIn; + this.ordToDoc = configuration.getDirectMonotonicReader(dataIn); + this.disi = configuration.getIndexedDISI(dataIn); + } + + @Override + public SparseOffHeapVectorValues copy() throws IOException { + return new SparseOffHeapVectorValues( + configuration, + dimension, + size, + centroid, + centroidDp, + binaryQuantizer, + dataIn, + similarityFunction, + vectorsScorer, + slice.clone() + ); + } + + @Override + public int ordToDoc(int ord) { + return (int) ordToDoc.get(ord); + } + + @Override + public Bits getAcceptOrds(Bits acceptDocs) { + if (acceptDocs == null) { + return null; + } + return new Bits() { + @Override + public boolean get(int index) { + return acceptDocs.get(ordToDoc(index)); + } + + @Override + public int length() { + return size; + } + }; + } + + @Override + public DocIndexIterator iterator() { + return IndexedDISI.asDocIndexIterator(disi); + } + + @Override + public VectorScorer scorer(float[] target) throws IOException { + SparseOffHeapVectorValues copy = copy(); + DocIndexIterator iterator = copy.iterator(); + RandomVectorScorer scorer = vectorsScorer.getRandomVectorScorer(similarityFunction, copy, target); + return new VectorScorer() { + @Override + public float score() throws IOException { + return scorer.score(iterator.index()); + } + + @Override + public DocIdSetIterator iterator() { + return iterator; + } + }; + } + } + + private static class EmptyOffHeapVectorValues extends OffHeapBinarizedVectorValues { + EmptyOffHeapVectorValues(int dimension, VectorSimilarityFunction similarityFunction, FlatVectorsScorer vectorsScorer) { + super(dimension, 0, null, Float.NaN, null, similarityFunction, vectorsScorer, null); + } + + @Override + public DocIndexIterator iterator() { + return createDenseIterator(); + } + + @Override + public DenseOffHeapVectorValues copy() { + throw new UnsupportedOperationException(); + } + + @Override + public Bits getAcceptOrds(Bits acceptDocs) { + return null; + } + + @Override + public VectorScorer scorer(float[] target) { + return null; + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizer.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizer.java new file mode 100644 index 000000000000..d5ed38cb5a0e --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizer.java @@ -0,0 +1,246 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.util.VectorUtil; + +import static org.apache.lucene.index.VectorSimilarityFunction.COSINE; +import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; + +class OptimizedScalarQuantizer { + // The initial interval is set to the minimum MSE grid for each number of bits + // these starting points are derived from the optimal MSE grid for a uniform distribution + static final float[][] MINIMUM_MSE_GRID = new float[][] { + { -0.798f, 0.798f }, + { -1.493f, 1.493f }, + { -2.051f, 2.051f }, + { -2.514f, 2.514f }, + { -2.916f, 2.916f }, + { -3.278f, 3.278f }, + { -3.611f, 3.611f }, + { -3.922f, 3.922f } }; + private static final float DEFAULT_LAMBDA = 0.1f; + private static final int DEFAULT_ITERS = 5; + private final VectorSimilarityFunction similarityFunction; + private final float lambda; + private final int iters; + + OptimizedScalarQuantizer(VectorSimilarityFunction similarityFunction, float lambda, int iters) { + this.similarityFunction = similarityFunction; + this.lambda = lambda; + this.iters = iters; + } + + OptimizedScalarQuantizer(VectorSimilarityFunction similarityFunction) { + this(similarityFunction, DEFAULT_LAMBDA, DEFAULT_ITERS); + } + + public record QuantizationResult(float lowerInterval, float upperInterval, float additionalCorrection, int quantizedComponentSum) {} + + public QuantizationResult[] multiScalarQuantize(float[] vector, byte[][] destinations, byte[] bits, float[] centroid) { + assert similarityFunction != COSINE || VectorUtil.isUnitVector(vector); + assert similarityFunction != COSINE || VectorUtil.isUnitVector(centroid); + assert bits.length == destinations.length; + float[] intervalScratch = new float[2]; + double vecMean = 0; + double vecVar = 0; + float norm2 = 0; + float centroidDot = 0; + float min = Float.MAX_VALUE; + float max = -Float.MAX_VALUE; + for (int i = 0; i < vector.length; ++i) { + if (similarityFunction != EUCLIDEAN) { + centroidDot += vector[i] * centroid[i]; + } + vector[i] = vector[i] - centroid[i]; + min = Math.min(min, vector[i]); + max = Math.max(max, vector[i]); + norm2 += (vector[i] * vector[i]); + double delta = vector[i] - vecMean; + vecMean += delta / (i + 1); + vecVar += delta * (vector[i] - vecMean); + } + vecVar /= vector.length; + double vecStd = Math.sqrt(vecVar); + QuantizationResult[] results = new QuantizationResult[bits.length]; + for (int i = 0; i < bits.length; ++i) { + assert bits[i] > 0 && bits[i] <= 8; + int points = (1 << bits[i]); + // Linearly scale the interval to the standard deviation of the vector, ensuring we are within the min/max bounds + intervalScratch[0] = (float) clamp((MINIMUM_MSE_GRID[bits[i] - 1][0] + vecMean) * vecStd, min, max); + intervalScratch[1] = (float) clamp((MINIMUM_MSE_GRID[bits[i] - 1][1] + vecMean) * vecStd, min, max); + optimizeIntervals(intervalScratch, vector, norm2, points); + float nSteps = ((1 << bits[i]) - 1); + float a = intervalScratch[0]; + float b = intervalScratch[1]; + float step = (b - a) / nSteps; + int sumQuery = 0; + // Now we have the optimized intervals, quantize the vector + for (int h = 0; h < vector.length; h++) { + float xi = (float) clamp(vector[h], a, b); + int assignment = Math.round((xi - a) / step); + sumQuery += assignment; + destinations[i][h] = (byte) assignment; + } + results[i] = new QuantizationResult( + intervalScratch[0], + intervalScratch[1], + similarityFunction == EUCLIDEAN ? norm2 : centroidDot, + sumQuery + ); + } + return results; + } + + public QuantizationResult scalarQuantize(float[] vector, byte[] destination, byte bits, float[] centroid) { + assert similarityFunction != COSINE || VectorUtil.isUnitVector(vector); + assert similarityFunction != COSINE || VectorUtil.isUnitVector(centroid); + assert vector.length <= destination.length; + assert bits > 0 && bits <= 8; + float[] intervalScratch = new float[2]; + int points = 1 << bits; + double vecMean = 0; + double vecVar = 0; + float norm2 = 0; + float centroidDot = 0; + float min = Float.MAX_VALUE; + float max = -Float.MAX_VALUE; + for (int i = 0; i < vector.length; ++i) { + if (similarityFunction != EUCLIDEAN) { + centroidDot += vector[i] * centroid[i]; + } + vector[i] = vector[i] - centroid[i]; + min = Math.min(min, vector[i]); + max = Math.max(max, vector[i]); + norm2 += (vector[i] * vector[i]); + double delta = vector[i] - vecMean; + vecMean += delta / (i + 1); + vecVar += delta * (vector[i] - vecMean); + } + vecVar /= vector.length; + double vecStd = Math.sqrt(vecVar); + // Linearly scale the interval to the standard deviation of the vector, ensuring we are within the min/max bounds + intervalScratch[0] = (float) clamp((MINIMUM_MSE_GRID[bits - 1][0] + vecMean) * vecStd, min, max); + intervalScratch[1] = (float) clamp((MINIMUM_MSE_GRID[bits - 1][1] + vecMean) * vecStd, min, max); + optimizeIntervals(intervalScratch, vector, norm2, points); + float nSteps = ((1 << bits) - 1); + // Now we have the optimized intervals, quantize the vector + float a = intervalScratch[0]; + float b = intervalScratch[1]; + float step = (b - a) / nSteps; + int sumQuery = 0; + for (int h = 0; h < vector.length; h++) { + float xi = (float) clamp(vector[h], a, b); + int assignment = Math.round((xi - a) / step); + sumQuery += assignment; + destination[h] = (byte) assignment; + } + return new QuantizationResult( + intervalScratch[0], + intervalScratch[1], + similarityFunction == EUCLIDEAN ? norm2 : centroidDot, + sumQuery + ); + } + + /** + * Compute the loss of the vector given the interval. Effectively, we are computing the MSE of a dequantized vector with the raw + * vector. + * @param vector raw vector + * @param interval interval to quantize the vector + * @param points number of quantization points + * @param norm2 squared norm of the vector + * @return the loss + */ + private double loss(float[] vector, float[] interval, int points, float norm2) { + double a = interval[0]; + double b = interval[1]; + double step = ((b - a) / (points - 1.0F)); + double stepInv = 1.0 / step; + double xe = 0.0; + double e = 0.0; + for (double xi : vector) { + // this is quantizing and then dequantizing the vector + double xiq = (a + step * Math.round((clamp(xi, a, b) - a) * stepInv)); + // how much does the de-quantized value differ from the original value + xe += xi * (xi - xiq); + e += (xi - xiq) * (xi - xiq); + } + return (1.0 - lambda) * xe * xe / norm2 + lambda * e; + } + + /** + * Optimize the quantization interval for the given vector. This is done via a coordinate descent trying to minimize the quantization + * loss. Note, the loss is not always guaranteed to decrease, so we have a maximum number of iterations and will exit early if the + * loss increases. + * @param initInterval initial interval, the optimized interval will be stored here + * @param vector raw vector + * @param norm2 squared norm of the vector + * @param points number of quantization points + */ + private void optimizeIntervals(float[] initInterval, float[] vector, float norm2, int points) { + double initialLoss = loss(vector, initInterval, points, norm2); + final float scale = (1.0f - lambda) / norm2; + if (Float.isFinite(scale) == false) { + return; + } + for (int i = 0; i < iters; ++i) { + float a = initInterval[0]; + float b = initInterval[1]; + float stepInv = (points - 1.0f) / (b - a); + // calculate the grid points for coordinate descent + double daa = 0; + double dab = 0; + double dbb = 0; + double dax = 0; + double dbx = 0; + for (float xi : vector) { + float k = Math.round((clamp(xi, a, b) - a) * stepInv); + float s = k / (points - 1); + daa += (1.0 - s) * (1.0 - s); + dab += (1.0 - s) * s; + dbb += s * s; + dax += xi * (1.0 - s); + dbx += xi * s; + } + double m0 = scale * dax * dax + lambda * daa; + double m1 = scale * dax * dbx + lambda * dab; + double m2 = scale * dbx * dbx + lambda * dbb; + // its possible that the determinant is 0, in which case we can't update the interval + double det = m0 * m2 - m1 * m1; + if (det == 0) { + return; + } + float aOpt = (float) ((m2 * dax - m1 * dbx) / det); + float bOpt = (float) ((m0 * dbx - m1 * dax) / det); + // If there is no change in the interval, we can stop + if ((Math.abs(initInterval[0] - aOpt) < 1e-8 && Math.abs(initInterval[1] - bOpt) < 1e-8)) { + return; + } + double newLoss = loss(vector, new float[] { aOpt, bOpt }, points, norm2); + // If the new loss is worse, don't update the interval and exit + // This optimization, unlike kMeans, does not always converge to better loss + // So exit if we are getting worse + if (newLoss > initialLoss) { + return; + } + // Update the interval and go again + initInterval[0] = aOpt; + initInterval[1] = bOpt; + initialLoss = newLoss; + } + } + + private static double clamp(double x, double a, double b) { + return Math.min(Math.max(x, a), b); + } + +} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 0a6a24f72757..d780faad96f2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -46,8 +46,8 @@ import org.elasticsearch.index.codec.vectors.ES814HnswScalarQuantizedVectorsFormat; import org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat; import org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat; -import org.elasticsearch.index.codec.vectors.es816.ES816BinaryQuantizedVectorsFormat; -import org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat; +import org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat; +import org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat; import org.elasticsearch.index.fielddata.FieldDataContext; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.mapper.ArraySourceValueFetcher; @@ -1788,7 +1788,7 @@ static class BBQHnswIndexOptions extends IndexOptions { @Override KnnVectorsFormat getVectorsFormat(ElementType elementType) { assert elementType == ElementType.FLOAT; - return new ES816HnswBinaryQuantizedVectorsFormat(m, efConstruction); + return new ES818HnswBinaryQuantizedVectorsFormat(m, efConstruction); } @Override @@ -1836,7 +1836,7 @@ static class BBQFlatIndexOptions extends IndexOptions { @Override KnnVectorsFormat getVectorsFormat(ElementType elementType) { assert elementType == ElementType.FLOAT; - return new ES816BinaryQuantizedVectorsFormat(); + return new ES818BinaryQuantizedVectorsFormat(); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java b/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java index 794b30aa5aab..57980321bdc3 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java @@ -44,6 +44,7 @@ private SearchCapabilities() {} private static final String MULTI_DENSE_VECTOR_SCRIPT_MAX_SIM = "multi_dense_vector_script_max_sim_with_bugfix"; private static final String RANDOM_SAMPLER_WITH_SCORED_SUBAGGS = "random_sampler_with_scored_subaggs"; + private static final String OPTIMIZED_SCALAR_QUANTIZATION_BBQ = "optimized_scalar_quantization_bbq"; public static final Set CAPABILITIES; static { @@ -55,6 +56,7 @@ private SearchCapabilities() {} capabilities.add(TRANSFORM_RANK_RRF_TO_RETRIEVER); capabilities.add(NESTED_RETRIEVER_INNER_HITS_SUPPORT); capabilities.add(RANDOM_SAMPLER_WITH_SCORED_SUBAGGS); + capabilities.add(OPTIMIZED_SCALAR_QUANTIZATION_BBQ); if (MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()) { capabilities.add(MULTI_DENSE_VECTOR_FIELD_MAPPER); capabilities.add(MULTI_DENSE_VECTOR_SCRIPT_ACCESS); diff --git a/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat b/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat index 389555e60b43..cef8d0998081 100644 --- a/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat +++ b/server/src/main/resources/META-INF/services/org.apache.lucene.codecs.KnnVectorsFormat @@ -5,3 +5,5 @@ org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat org.elasticsearch.index.codec.vectors.es816.ES816BinaryQuantizedVectorsFormat org.elasticsearch.index.codec.vectors.es816.ES816HnswBinaryQuantizedVectorsFormat +org.elasticsearch.index.codec.vectors.es818.ES818BinaryQuantizedVectorsFormat +org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormat diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/BQVectorUtilsTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/BQVectorUtilsTests.java index 9f9114c70b6d..270ad54e9a96 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/BQVectorUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/BQVectorUtilsTests.java @@ -38,6 +38,32 @@ public static int popcount(byte[] a, int aOffset, byte[] b, int length) { private static float DELTA = Float.MIN_VALUE; + public void testPackAsBinary() { + // 5 bits + byte[] toPack = new byte[] { 1, 1, 0, 0, 1 }; + byte[] packed = new byte[1]; + BQVectorUtils.packAsBinary(toPack, packed); + assertArrayEquals(new byte[] { (byte) 0b11001000 }, packed); + + // 8 bits + toPack = new byte[] { 1, 1, 0, 0, 1, 0, 1, 0 }; + packed = new byte[1]; + BQVectorUtils.packAsBinary(toPack, packed); + assertArrayEquals(new byte[] { (byte) 0b11001010 }, packed); + + // 10 bits + toPack = new byte[] { 1, 1, 0, 0, 1, 0, 1, 0, 1, 1 }; + packed = new byte[2]; + BQVectorUtils.packAsBinary(toPack, packed); + assertArrayEquals(new byte[] { (byte) 0b11001010, (byte) 0b11000000 }, packed); + + // 16 bits + toPack = new byte[] { 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0 }; + packed = new byte[2]; + BQVectorUtils.packAsBinary(toPack, packed); + assertArrayEquals(new byte[] { (byte) 0b11001010, (byte) 0b11100110 }, packed); + } + public void testPadFloat() { assertArrayEquals(new float[] { 1, 2, 3, 4 }, BQVectorUtils.pad(new float[] { 1, 2, 3, 4 }, 4), DELTA); assertArrayEquals(new float[] { 1, 2, 3, 4 }, BQVectorUtils.pad(new float[] { 1, 2, 3, 4 }, 3), DELTA); diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatRWVectorsScorer.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatRWVectorsScorer.java new file mode 100644 index 000000000000..0bebe16f468c --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatRWVectorsScorer.java @@ -0,0 +1,256 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es816; + +import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; +import org.apache.lucene.index.KnnVectorValues; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.VectorUtil; +import org.apache.lucene.util.hnsw.RandomVectorScorer; +import org.apache.lucene.util.hnsw.RandomVectorScorerSupplier; +import org.elasticsearch.index.codec.vectors.BQSpaceUtils; +import org.elasticsearch.index.codec.vectors.BQVectorUtils; +import org.elasticsearch.simdvec.ESVectorUtil; + +import java.io.IOException; + +import static org.apache.lucene.index.VectorSimilarityFunction.COSINE; +import static org.apache.lucene.index.VectorSimilarityFunction.EUCLIDEAN; +import static org.apache.lucene.index.VectorSimilarityFunction.MAXIMUM_INNER_PRODUCT; + +/** Vector scorer over binarized vector values */ +class ES816BinaryFlatRWVectorsScorer implements FlatVectorsScorer { + private final FlatVectorsScorer nonQuantizedDelegate; + + ES816BinaryFlatRWVectorsScorer(FlatVectorsScorer nonQuantizedDelegate) { + this.nonQuantizedDelegate = nonQuantizedDelegate; + } + + @Override + public RandomVectorScorerSupplier getRandomVectorScorerSupplier( + VectorSimilarityFunction similarityFunction, + KnnVectorValues vectorValues + ) throws IOException { + if (vectorValues instanceof BinarizedByteVectorValues) { + throw new UnsupportedOperationException( + "getRandomVectorScorerSupplier(VectorSimilarityFunction,RandomAccessVectorValues) not implemented for binarized format" + ); + } + return nonQuantizedDelegate.getRandomVectorScorerSupplier(similarityFunction, vectorValues); + } + + @Override + public RandomVectorScorer getRandomVectorScorer( + VectorSimilarityFunction similarityFunction, + KnnVectorValues vectorValues, + float[] target + ) throws IOException { + if (vectorValues instanceof BinarizedByteVectorValues binarizedVectors) { + BinaryQuantizer quantizer = binarizedVectors.getQuantizer(); + float[] centroid = binarizedVectors.getCentroid(); + // FIXME: precompute this once? + int discretizedDimensions = BQVectorUtils.discretize(target.length, 64); + if (similarityFunction == COSINE) { + float[] copy = ArrayUtil.copyOfSubArray(target, 0, target.length); + VectorUtil.l2normalize(copy); + target = copy; + } + byte[] quantized = new byte[BQSpaceUtils.B_QUERY * discretizedDimensions / 8]; + BinaryQuantizer.QueryFactors factors = quantizer.quantizeForQuery(target, quantized, centroid); + BinaryQueryVector queryVector = new BinaryQueryVector(quantized, factors); + return new BinarizedRandomVectorScorer(queryVector, binarizedVectors, similarityFunction); + } + return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target); + } + + @Override + public RandomVectorScorer getRandomVectorScorer( + VectorSimilarityFunction similarityFunction, + KnnVectorValues vectorValues, + byte[] target + ) throws IOException { + return nonQuantizedDelegate.getRandomVectorScorer(similarityFunction, vectorValues, target); + } + + RandomVectorScorerSupplier getRandomVectorScorerSupplier( + VectorSimilarityFunction similarityFunction, + ES816BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues scoringVectors, + BinarizedByteVectorValues targetVectors + ) { + return new BinarizedRandomVectorScorerSupplier(scoringVectors, targetVectors, similarityFunction); + } + + @Override + public String toString() { + return "ES816BinaryFlatVectorsScorer(nonQuantizedDelegate=" + nonQuantizedDelegate + ")"; + } + + /** Vector scorer supplier over binarized vector values */ + static class BinarizedRandomVectorScorerSupplier implements RandomVectorScorerSupplier { + private final ES816BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors; + private final BinarizedByteVectorValues targetVectors; + private final VectorSimilarityFunction similarityFunction; + + BinarizedRandomVectorScorerSupplier( + ES816BinaryQuantizedVectorsWriter.OffHeapBinarizedQueryVectorValues queryVectors, + BinarizedByteVectorValues targetVectors, + VectorSimilarityFunction similarityFunction + ) { + this.queryVectors = queryVectors; + this.targetVectors = targetVectors; + this.similarityFunction = similarityFunction; + } + + @Override + public RandomVectorScorer scorer(int ord) throws IOException { + byte[] vector = queryVectors.vectorValue(ord); + int quantizedSum = queryVectors.sumQuantizedValues(ord); + float distanceToCentroid = queryVectors.getCentroidDistance(ord); + float lower = queryVectors.getLower(ord); + float width = queryVectors.getWidth(ord); + float normVmC = 0f; + float vDotC = 0f; + if (similarityFunction != EUCLIDEAN) { + normVmC = queryVectors.getNormVmC(ord); + vDotC = queryVectors.getVDotC(ord); + } + BinaryQueryVector binaryQueryVector = new BinaryQueryVector( + vector, + new BinaryQuantizer.QueryFactors(quantizedSum, distanceToCentroid, lower, width, normVmC, vDotC) + ); + return new BinarizedRandomVectorScorer(binaryQueryVector, targetVectors, similarityFunction); + } + + @Override + public RandomVectorScorerSupplier copy() throws IOException { + return new BinarizedRandomVectorScorerSupplier(queryVectors.copy(), targetVectors.copy(), similarityFunction); + } + } + + /** A binarized query representing its quantized form along with factors */ + record BinaryQueryVector(byte[] vector, BinaryQuantizer.QueryFactors factors) {} + + /** Vector scorer over binarized vector values */ + static class BinarizedRandomVectorScorer extends RandomVectorScorer.AbstractRandomVectorScorer { + private final BinaryQueryVector queryVector; + private final BinarizedByteVectorValues targetVectors; + private final VectorSimilarityFunction similarityFunction; + + private final float sqrtDimensions; + private final float maxX1; + + BinarizedRandomVectorScorer( + BinaryQueryVector queryVectors, + BinarizedByteVectorValues targetVectors, + VectorSimilarityFunction similarityFunction + ) { + super(targetVectors); + this.queryVector = queryVectors; + this.targetVectors = targetVectors; + this.similarityFunction = similarityFunction; + // FIXME: precompute this once? + this.sqrtDimensions = targetVectors.sqrtDimensions(); + this.maxX1 = targetVectors.maxX1(); + } + + @Override + public float score(int targetOrd) throws IOException { + byte[] quantizedQuery = queryVector.vector(); + int quantizedSum = queryVector.factors().quantizedSum(); + float lower = queryVector.factors().lower(); + float width = queryVector.factors().width(); + float distanceToCentroid = queryVector.factors().distToC(); + if (similarityFunction == EUCLIDEAN) { + return euclideanScore(targetOrd, sqrtDimensions, quantizedQuery, distanceToCentroid, lower, quantizedSum, width); + } + + float vmC = queryVector.factors().normVmC(); + float vDotC = queryVector.factors().vDotC(); + float cDotC = targetVectors.getCentroidDP(); + byte[] binaryCode = targetVectors.vectorValue(targetOrd); + float ooq = targetVectors.getOOQ(targetOrd); + float normOC = targetVectors.getNormOC(targetOrd); + float oDotC = targetVectors.getODotC(targetOrd); + + float qcDist = ESVectorUtil.ipByteBinByte(quantizedQuery, binaryCode); + + // FIXME: pre-compute these only once for each target vector + // ... pull this out or use a similar cache mechanism as do in score + float xbSum = (float) BQVectorUtils.popcount(binaryCode); + final float dist; + // If ||o-c|| == 0, so, it's ok to throw the rest of the equation away + // and simply use `oDotC + vDotC - cDotC` as centroid == doc vector + if (normOC == 0 || ooq == 0) { + dist = oDotC + vDotC - cDotC; + } else { + // If ||o-c|| != 0, we should assume that `ooq` is finite + assert Float.isFinite(ooq); + float estimatedDot = (2 * width / sqrtDimensions * qcDist + 2 * lower / sqrtDimensions * xbSum - width / sqrtDimensions + * quantizedSum - sqrtDimensions * lower) / ooq; + dist = vmC * normOC * estimatedDot + oDotC + vDotC - cDotC; + } + assert Float.isFinite(dist); + + float ooqSqr = (float) Math.pow(ooq, 2); + float errorBound = (float) (vmC * normOC * (maxX1 * Math.sqrt((1 - ooqSqr) / ooqSqr))); + float score = Float.isFinite(errorBound) ? dist - errorBound : dist; + if (similarityFunction == MAXIMUM_INNER_PRODUCT) { + return VectorUtil.scaleMaxInnerProductScore(score); + } + return Math.max((1f + score) / 2f, 0); + } + + private float euclideanScore( + int targetOrd, + float sqrtDimensions, + byte[] quantizedQuery, + float distanceToCentroid, + float lower, + int quantizedSum, + float width + ) throws IOException { + byte[] binaryCode = targetVectors.vectorValue(targetOrd); + + // FIXME: pre-compute these only once for each target vector + // .. not sure how to enumerate the target ordinals but that's what we did in PoC + float targetDistToC = targetVectors.getCentroidDistance(targetOrd); + float x0 = targetVectors.getVectorMagnitude(targetOrd); + float sqrX = targetDistToC * targetDistToC; + double xX0 = targetDistToC / x0; + + // TODO maybe store? + float xbSum = (float) BQVectorUtils.popcount(binaryCode); + float factorPPC = (float) (-2.0 / sqrtDimensions * xX0 * (xbSum * 2.0 - targetVectors.dimension())); + float factorIP = (float) (-2.0 / sqrtDimensions * xX0); + + long qcDist = ESVectorUtil.ipByteBinByte(quantizedQuery, binaryCode); + float score = sqrX + distanceToCentroid + factorPPC * lower + (qcDist * 2 - quantizedSum) * factorIP * width; + float projectionDist = (float) Math.sqrt(xX0 * xX0 - targetDistToC * targetDistToC); + float error = 2.0f * maxX1 * projectionDist; + float y = (float) Math.sqrt(distanceToCentroid); + float errorBound = y * error; + if (Float.isFinite(errorBound)) { + score = score + errorBound; + } + return Math.max(1 / (1f + score), 0); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorerTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorerTests.java index a75b9bc6064d..ffe007be9799 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorerTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryFlatVectorsScorerTests.java @@ -59,7 +59,7 @@ public void testScore() throws IOException { short quantizedSum = (short) random().nextInt(0, 4097); float normVmC = random().nextFloat(-1000f, 1000f); float vDotC = random().nextFloat(-1000f, 1000f); - ES816BinaryFlatVectorsScorer.BinaryQueryVector queryVector = new ES816BinaryFlatVectorsScorer.BinaryQueryVector( + ES816BinaryFlatRWVectorsScorer.BinaryQueryVector queryVector = new ES816BinaryFlatRWVectorsScorer.BinaryQueryVector( vector, new BinaryQuantizer.QueryFactors(quantizedSum, distanceToCentroid, vl, width, normVmC, vDotC) ); @@ -134,7 +134,7 @@ public int dimension() { } }; - ES816BinaryFlatVectorsScorer.BinarizedRandomVectorScorer scorer = new ES816BinaryFlatVectorsScorer.BinarizedRandomVectorScorer( + ES816BinaryFlatRWVectorsScorer.BinarizedRandomVectorScorer scorer = new ES816BinaryFlatRWVectorsScorer.BinarizedRandomVectorScorer( queryVector, targetVectors, similarityFunction @@ -217,7 +217,7 @@ public void testScoreEuclidean() throws IOException { float vl = -57.883f; float width = 9.972266f; short quantizedSum = 795; - ES816BinaryFlatVectorsScorer.BinaryQueryVector queryVector = new ES816BinaryFlatVectorsScorer.BinaryQueryVector( + ES816BinaryFlatRWVectorsScorer.BinaryQueryVector queryVector = new ES816BinaryFlatRWVectorsScorer.BinaryQueryVector( vector, new BinaryQuantizer.QueryFactors(quantizedSum, distanceToCentroid, vl, width, 0f, 0f) ); @@ -420,7 +420,7 @@ public int dimension() { VectorSimilarityFunction similarityFunction = VectorSimilarityFunction.EUCLIDEAN; - ES816BinaryFlatVectorsScorer.BinarizedRandomVectorScorer scorer = new ES816BinaryFlatVectorsScorer.BinarizedRandomVectorScorer( + ES816BinaryFlatRWVectorsScorer.BinarizedRandomVectorScorer scorer = new ES816BinaryFlatRWVectorsScorer.BinarizedRandomVectorScorer( queryVector, targetVectors, similarityFunction @@ -824,7 +824,7 @@ public void testScoreMIP() throws IOException { float normVmC = 9.766797f; float vDotC = 133.56123f; float cDotC = 132.20227f; - ES816BinaryFlatVectorsScorer.BinaryQueryVector queryVector = new ES816BinaryFlatVectorsScorer.BinaryQueryVector( + ES816BinaryFlatRWVectorsScorer.BinaryQueryVector queryVector = new ES816BinaryFlatRWVectorsScorer.BinaryQueryVector( vector, new BinaryQuantizer.QueryFactors(quantizedSum, distanceToCentroid, vl, width, normVmC, vDotC) ); @@ -1768,7 +1768,7 @@ public int dimension() { VectorSimilarityFunction similarityFunction = VectorSimilarityFunction.MAXIMUM_INNER_PRODUCT; - ES816BinaryFlatVectorsScorer.BinarizedRandomVectorScorer scorer = new ES816BinaryFlatVectorsScorer.BinarizedRandomVectorScorer( + ES816BinaryFlatRWVectorsScorer.BinarizedRandomVectorScorer scorer = new ES816BinaryFlatRWVectorsScorer.BinarizedRandomVectorScorer( queryVector, targetVectors, similarityFunction diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedRWVectorsFormat.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedRWVectorsFormat.java new file mode 100644 index 000000000000..c54903a94b54 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedRWVectorsFormat.java @@ -0,0 +1,52 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es816; + +import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; +import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat; +import org.apache.lucene.index.SegmentWriteState; + +import java.io.IOException; + +/** + * Copied from Lucene, replace with Lucene's implementation sometime after Lucene 10 + */ +public class ES816BinaryQuantizedRWVectorsFormat extends ES816BinaryQuantizedVectorsFormat { + + private static final FlatVectorsFormat rawVectorFormat = new Lucene99FlatVectorsFormat( + FlatVectorScorerUtil.getLucene99FlatVectorsScorer() + ); + + private static final ES816BinaryFlatRWVectorsScorer scorer = new ES816BinaryFlatRWVectorsScorer( + FlatVectorScorerUtil.getLucene99FlatVectorsScorer() + ); + + /** Creates a new instance with the default number of vectors per cluster. */ + public ES816BinaryQuantizedRWVectorsFormat() { + super(); + } + + @Override + public FlatVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + return new ES816BinaryQuantizedVectorsWriter(scorer, rawVectorFormat.fieldsWriter(state), state); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormatTests.java index 681f615653d4..48ba566353f5 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsFormatTests.java @@ -63,7 +63,7 @@ protected Codec getCodec() { return new Lucene100Codec() { @Override public KnnVectorsFormat getKnnVectorsFormatForField(String field) { - return new ES816BinaryQuantizedVectorsFormat(); + return new ES816BinaryQuantizedRWVectorsFormat(); } }; } diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java similarity index 99% rename from server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java rename to server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java index 31ae977e8111..4d97235c5fae 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816BinaryQuantizedVectorsWriter.java @@ -77,7 +77,7 @@ class ES816BinaryQuantizedVectorsWriter extends FlatVectorsWriter { private final List fields = new ArrayList<>(); private final IndexOutput meta, binarizedVectorData; private final FlatVectorsWriter rawVectorDelegate; - private final ES816BinaryFlatVectorsScorer vectorsScorer; + private final ES816BinaryFlatRWVectorsScorer vectorsScorer; private boolean finished; /** @@ -86,7 +86,7 @@ class ES816BinaryQuantizedVectorsWriter extends FlatVectorsWriter { * @param vectorsScorer the scorer to use for scoring vectors */ protected ES816BinaryQuantizedVectorsWriter( - ES816BinaryFlatVectorsScorer vectorsScorer, + ES816BinaryFlatRWVectorsScorer vectorsScorer, FlatVectorsWriter rawVectorDelegate, SegmentWriteState state ) throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedRWVectorsFormat.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedRWVectorsFormat.java new file mode 100644 index 000000000000..e9bace72b591 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedRWVectorsFormat.java @@ -0,0 +1,55 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es816; + +import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsWriter; +import org.apache.lucene.index.SegmentWriteState; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN; +import static org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat.DEFAULT_NUM_MERGE_WORKER; + +class ES816HnswBinaryQuantizedRWVectorsFormat extends ES816HnswBinaryQuantizedVectorsFormat { + + private static final FlatVectorsFormat flatVectorsFormat = new ES816BinaryQuantizedRWVectorsFormat(); + + /** Constructs a format using default graph construction parameters */ + ES816HnswBinaryQuantizedRWVectorsFormat() { + this(DEFAULT_MAX_CONN, DEFAULT_BEAM_WIDTH); + } + + ES816HnswBinaryQuantizedRWVectorsFormat(int maxConn, int beamWidth) { + this(maxConn, beamWidth, DEFAULT_NUM_MERGE_WORKER, null); + } + + ES816HnswBinaryQuantizedRWVectorsFormat(int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) { + super(maxConn, beamWidth, numMergeWorkers, mergeExec); + } + + @Override + public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException { + return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), 1, null); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormatTests.java index a25fa2836ee3..03aa847f3a5d 100644 --- a/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormatTests.java +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es816/ES816HnswBinaryQuantizedVectorsFormatTests.java @@ -59,7 +59,7 @@ protected Codec getCodec() { return new Lucene100Codec() { @Override public KnnVectorsFormat getKnnVectorsFormatForField(String field) { - return new ES816HnswBinaryQuantizedVectorsFormat(); + return new ES816HnswBinaryQuantizedRWVectorsFormat(); } }; } diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java new file mode 100644 index 000000000000..397cc472592b --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818BinaryQuantizedVectorsFormatTests.java @@ -0,0 +1,181 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.FilterCodec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.lucene100.Lucene100Codec; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.KnnFloatVectorField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.KnnVectorValues; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.KnnFloatVectorQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TotalHits; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; +import org.elasticsearch.common.logging.LogConfigurator; +import org.elasticsearch.index.codec.vectors.BQVectorUtils; + +import java.io.IOException; +import java.util.Locale; + +import static java.lang.String.format; +import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.oneOf; + +public class ES818BinaryQuantizedVectorsFormatTests extends BaseKnnVectorsFormatTestCase { + + static { + LogConfigurator.loadLog4jPlugins(); + LogConfigurator.configureESLogging(); // native access requires logging to be initialized + } + + @Override + protected Codec getCodec() { + return new Lucene100Codec() { + @Override + public KnnVectorsFormat getKnnVectorsFormatForField(String field) { + return new ES818BinaryQuantizedVectorsFormat(); + } + }; + } + + public void testSearch() throws Exception { + String fieldName = "field"; + int numVectors = random().nextInt(99, 500); + int dims = random().nextInt(4, 65); + float[] vector = randomVector(dims); + VectorSimilarityFunction similarityFunction = randomSimilarity(); + KnnFloatVectorField knnField = new KnnFloatVectorField(fieldName, vector, similarityFunction); + IndexWriterConfig iwc = newIndexWriterConfig(); + try (Directory dir = newDirectory()) { + try (IndexWriter w = new IndexWriter(dir, iwc)) { + for (int i = 0; i < numVectors; i++) { + Document doc = new Document(); + knnField.setVectorValue(randomVector(dims)); + doc.add(knnField); + w.addDocument(doc); + } + w.commit(); + + try (IndexReader reader = DirectoryReader.open(w)) { + IndexSearcher searcher = new IndexSearcher(reader); + final int k = random().nextInt(5, 50); + float[] queryVector = randomVector(dims); + Query q = new KnnFloatVectorQuery(fieldName, queryVector, k); + TopDocs collectedDocs = searcher.search(q, k); + assertEquals(k, collectedDocs.totalHits.value()); + assertEquals(TotalHits.Relation.EQUAL_TO, collectedDocs.totalHits.relation()); + } + } + } + } + + public void testToString() { + FilterCodec customCodec = new FilterCodec("foo", Codec.getDefault()) { + @Override + public KnnVectorsFormat knnVectorsFormat() { + return new ES818BinaryQuantizedVectorsFormat(); + } + }; + String expectedPattern = "ES818BinaryQuantizedVectorsFormat(" + + "name=ES818BinaryQuantizedVectorsFormat, " + + "flatVectorScorer=ES818BinaryFlatVectorsScorer(nonQuantizedDelegate=%s()))"; + var defaultScorer = format(Locale.ROOT, expectedPattern, "DefaultFlatVectorScorer"); + var memSegScorer = format(Locale.ROOT, expectedPattern, "Lucene99MemorySegmentFlatVectorsScorer"); + assertThat(customCodec.knnVectorsFormat().toString(), is(oneOf(defaultScorer, memSegScorer))); + } + + @Override + public void testRandomWithUpdatesAndGraph() { + // graph not supported + } + + @Override + public void testSearchWithVisitedLimit() { + // visited limit is not respected, as it is brute force search + } + + public void testQuantizedVectorsWriteAndRead() throws IOException { + String fieldName = "field"; + int numVectors = random().nextInt(99, 500); + int dims = random().nextInt(4, 65); + + float[] vector = randomVector(dims); + VectorSimilarityFunction similarityFunction = randomSimilarity(); + KnnFloatVectorField knnField = new KnnFloatVectorField(fieldName, vector, similarityFunction); + try (Directory dir = newDirectory()) { + try (IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) { + for (int i = 0; i < numVectors; i++) { + Document doc = new Document(); + knnField.setVectorValue(randomVector(dims)); + doc.add(knnField); + w.addDocument(doc); + if (i % 101 == 0) { + w.commit(); + } + } + w.commit(); + w.forceMerge(1); + + try (IndexReader reader = DirectoryReader.open(w)) { + LeafReader r = getOnlyLeafReader(reader); + FloatVectorValues vectorValues = r.getFloatVectorValues(fieldName); + assertEquals(vectorValues.size(), numVectors); + BinarizedByteVectorValues qvectorValues = ((ES818BinaryQuantizedVectorsReader.BinarizedVectorValues) vectorValues) + .getQuantizedVectorValues(); + float[] centroid = qvectorValues.getCentroid(); + assertEquals(centroid.length, dims); + + OptimizedScalarQuantizer quantizer = new OptimizedScalarQuantizer(similarityFunction); + byte[] quantizedVector = new byte[dims]; + byte[] expectedVector = new byte[BQVectorUtils.discretize(dims, 64) / 8]; + if (similarityFunction == VectorSimilarityFunction.COSINE) { + vectorValues = new ES818BinaryQuantizedVectorsWriter.NormalizedFloatVectorValues(vectorValues); + } + KnnVectorValues.DocIndexIterator docIndexIterator = vectorValues.iterator(); + + while (docIndexIterator.nextDoc() != NO_MORE_DOCS) { + OptimizedScalarQuantizer.QuantizationResult corrections = quantizer.scalarQuantize( + vectorValues.vectorValue(docIndexIterator.index()), + quantizedVector, + (byte) 1, + centroid + ); + BQVectorUtils.packAsBinary(quantizedVector, expectedVector); + assertArrayEquals(expectedVector, qvectorValues.vectorValue(docIndexIterator.index())); + assertEquals(corrections, qvectorValues.getCorrectiveTerms(docIndexIterator.index())); + } + } + } + } + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java new file mode 100644 index 000000000000..b6ae3199bb89 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/ES818HnswBinaryQuantizedVectorsFormatTests.java @@ -0,0 +1,132 @@ +/* + * @notice + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2024 Elasticsearch B.V. + */ +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.FilterCodec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.codecs.lucene100.Lucene100Codec; +import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.KnnFloatVectorField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.KnnVectorValues; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.index.BaseKnnVectorsFormatTestCase; +import org.apache.lucene.util.SameThreadExecutorService; +import org.elasticsearch.common.logging.LogConfigurator; + +import java.util.Arrays; +import java.util.Locale; + +import static java.lang.String.format; +import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.oneOf; + +public class ES818HnswBinaryQuantizedVectorsFormatTests extends BaseKnnVectorsFormatTestCase { + + static { + LogConfigurator.loadLog4jPlugins(); + LogConfigurator.configureESLogging(); // native access requires logging to be initialized + } + + @Override + protected Codec getCodec() { + return new Lucene100Codec() { + @Override + public KnnVectorsFormat getKnnVectorsFormatForField(String field) { + return new ES818HnswBinaryQuantizedVectorsFormat(); + } + }; + } + + public void testToString() { + FilterCodec customCodec = new FilterCodec("foo", Codec.getDefault()) { + @Override + public KnnVectorsFormat knnVectorsFormat() { + return new ES818HnswBinaryQuantizedVectorsFormat(10, 20, 1, null); + } + }; + String expectedPattern = + "ES818HnswBinaryQuantizedVectorsFormat(name=ES818HnswBinaryQuantizedVectorsFormat, maxConn=10, beamWidth=20," + + " flatVectorFormat=ES818BinaryQuantizedVectorsFormat(name=ES818BinaryQuantizedVectorsFormat," + + " flatVectorScorer=ES818BinaryFlatVectorsScorer(nonQuantizedDelegate=%s())))"; + + var defaultScorer = format(Locale.ROOT, expectedPattern, "DefaultFlatVectorScorer"); + var memSegScorer = format(Locale.ROOT, expectedPattern, "Lucene99MemorySegmentFlatVectorsScorer"); + assertThat(customCodec.knnVectorsFormat().toString(), is(oneOf(defaultScorer, memSegScorer))); + } + + public void testSingleVectorCase() throws Exception { + float[] vector = randomVector(random().nextInt(12, 500)); + for (VectorSimilarityFunction similarityFunction : VectorSimilarityFunction.values()) { + try (Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) { + Document doc = new Document(); + doc.add(new KnnFloatVectorField("f", vector, similarityFunction)); + w.addDocument(doc); + w.commit(); + try (IndexReader reader = DirectoryReader.open(w)) { + LeafReader r = getOnlyLeafReader(reader); + FloatVectorValues vectorValues = r.getFloatVectorValues("f"); + KnnVectorValues.DocIndexIterator docIndexIterator = vectorValues.iterator(); + assert (vectorValues.size() == 1); + while (docIndexIterator.nextDoc() != NO_MORE_DOCS) { + assertArrayEquals(vector, vectorValues.vectorValue(docIndexIterator.index()), 0.00001f); + } + float[] randomVector = randomVector(vector.length); + float trueScore = similarityFunction.compare(vector, randomVector); + TopDocs td = r.searchNearestVectors("f", randomVector, 1, null, Integer.MAX_VALUE); + assertEquals(1, td.totalHits.value()); + assertTrue(td.scoreDocs[0].score >= 0); + // When it's the only vector in a segment, the score should be very close to the true score + assertEquals(trueScore, td.scoreDocs[0].score, 0.0001f); + } + } + } + } + + public void testLimits() { + expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(-1, 20)); + expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(0, 20)); + expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 0)); + expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(20, -1)); + expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(512 + 1, 20)); + expectThrows(IllegalArgumentException.class, () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 3201)); + expectThrows( + IllegalArgumentException.class, + () -> new ES818HnswBinaryQuantizedVectorsFormat(20, 100, 1, new SameThreadExecutorService()) + ); + } + + // Ensures that all expected vector similarity functions are translatable in the format. + public void testVectorSimilarityFuncs() { + // This does not necessarily have to be all similarity functions, but + // differences should be considered carefully. + var expectedValues = Arrays.stream(VectorSimilarityFunction.values()).toList(); + assertEquals(Lucene99HnswVectorsReader.SIMILARITY_FUNCTIONS, expectedValues); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizerTests.java b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizerTests.java new file mode 100644 index 000000000000..e3e2d6caafe0 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/vectors/es818/OptimizedScalarQuantizerTests.java @@ -0,0 +1,136 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.codec.vectors.es818; + +import org.apache.lucene.index.VectorSimilarityFunction; +import org.apache.lucene.util.VectorUtil; +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.index.codec.vectors.es818.OptimizedScalarQuantizer.MINIMUM_MSE_GRID; + +public class OptimizedScalarQuantizerTests extends ESTestCase { + + static final byte[] ALL_BITS = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + + public void testAbusiveEdgeCases() { + // large zero array + for (VectorSimilarityFunction vectorSimilarityFunction : VectorSimilarityFunction.values()) { + if (vectorSimilarityFunction == VectorSimilarityFunction.COSINE) { + continue; + } + float[] vector = new float[4096]; + float[] centroid = new float[4096]; + OptimizedScalarQuantizer osq = new OptimizedScalarQuantizer(vectorSimilarityFunction); + byte[][] destinations = new byte[MINIMUM_MSE_GRID.length][4096]; + OptimizedScalarQuantizer.QuantizationResult[] results = osq.multiScalarQuantize(vector, destinations, ALL_BITS, centroid); + assertEquals(MINIMUM_MSE_GRID.length, results.length); + assertValidResults(results); + for (byte[] destination : destinations) { + assertArrayEquals(new byte[4096], destination); + } + byte[] destination = new byte[4096]; + for (byte bit : ALL_BITS) { + OptimizedScalarQuantizer.QuantizationResult result = osq.scalarQuantize(vector, destination, bit, centroid); + assertValidResults(result); + assertArrayEquals(new byte[4096], destination); + } + } + + // single value array + for (VectorSimilarityFunction vectorSimilarityFunction : VectorSimilarityFunction.values()) { + float[] vector = new float[] { randomFloat() }; + float[] centroid = new float[] { randomFloat() }; + if (vectorSimilarityFunction == VectorSimilarityFunction.COSINE) { + VectorUtil.l2normalize(vector); + VectorUtil.l2normalize(centroid); + } + OptimizedScalarQuantizer osq = new OptimizedScalarQuantizer(vectorSimilarityFunction); + byte[][] destinations = new byte[MINIMUM_MSE_GRID.length][1]; + OptimizedScalarQuantizer.QuantizationResult[] results = osq.multiScalarQuantize(vector, destinations, ALL_BITS, centroid); + assertEquals(MINIMUM_MSE_GRID.length, results.length); + assertValidResults(results); + for (int i = 0; i < ALL_BITS.length; i++) { + assertValidQuantizedRange(destinations[i], ALL_BITS[i]); + } + for (byte bit : ALL_BITS) { + vector = new float[] { randomFloat() }; + centroid = new float[] { randomFloat() }; + if (vectorSimilarityFunction == VectorSimilarityFunction.COSINE) { + VectorUtil.l2normalize(vector); + VectorUtil.l2normalize(centroid); + } + byte[] destination = new byte[1]; + OptimizedScalarQuantizer.QuantizationResult result = osq.scalarQuantize(vector, destination, bit, centroid); + assertValidResults(result); + assertValidQuantizedRange(destination, bit); + } + } + + } + + public void testMathematicalConsistency() { + int dims = randomIntBetween(1, 4096); + float[] vector = new float[dims]; + for (int i = 0; i < dims; ++i) { + vector[i] = randomFloat(); + } + float[] centroid = new float[dims]; + for (int i = 0; i < dims; ++i) { + centroid[i] = randomFloat(); + } + float[] copy = new float[dims]; + for (VectorSimilarityFunction vectorSimilarityFunction : VectorSimilarityFunction.values()) { + // copy the vector to avoid modifying it + System.arraycopy(vector, 0, copy, 0, dims); + if (vectorSimilarityFunction == VectorSimilarityFunction.COSINE) { + VectorUtil.l2normalize(copy); + VectorUtil.l2normalize(centroid); + } + OptimizedScalarQuantizer osq = new OptimizedScalarQuantizer(vectorSimilarityFunction); + byte[][] destinations = new byte[MINIMUM_MSE_GRID.length][dims]; + OptimizedScalarQuantizer.QuantizationResult[] results = osq.multiScalarQuantize(copy, destinations, ALL_BITS, centroid); + assertEquals(MINIMUM_MSE_GRID.length, results.length); + assertValidResults(results); + for (int i = 0; i < ALL_BITS.length; i++) { + assertValidQuantizedRange(destinations[i], ALL_BITS[i]); + } + for (byte bit : ALL_BITS) { + byte[] destination = new byte[dims]; + System.arraycopy(vector, 0, copy, 0, dims); + if (vectorSimilarityFunction == VectorSimilarityFunction.COSINE) { + VectorUtil.l2normalize(copy); + VectorUtil.l2normalize(centroid); + } + OptimizedScalarQuantizer.QuantizationResult result = osq.scalarQuantize(copy, destination, bit, centroid); + assertValidResults(result); + assertValidQuantizedRange(destination, bit); + } + } + } + + static void assertValidQuantizedRange(byte[] quantized, byte bits) { + for (byte b : quantized) { + if (bits < 8) { + assertTrue(b >= 0); + } + assertTrue(b < 1 << bits); + } + } + + static void assertValidResults(OptimizedScalarQuantizer.QuantizationResult... results) { + for (OptimizedScalarQuantizer.QuantizationResult result : results) { + assertTrue(Float.isFinite(result.lowerInterval())); + assertTrue(Float.isFinite(result.upperInterval())); + assertTrue(result.lowerInterval() <= result.upperInterval()); + assertTrue(Float.isFinite(result.additionalCorrection())); + assertTrue(result.quantizedComponentSum() >= 0); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java index de084cd4582e..c043b9ffb381 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java @@ -1970,13 +1970,13 @@ public void testKnnBBQHNSWVectorsFormat() throws IOException { assertThat(codec, instanceOf(LegacyPerFieldMapperCodec.class)); knnVectorsFormat = ((LegacyPerFieldMapperCodec) codec).getKnnVectorsFormatForField("field"); } - String expectedString = "ES816HnswBinaryQuantizedVectorsFormat(name=ES816HnswBinaryQuantizedVectorsFormat, maxConn=" + String expectedString = "ES818HnswBinaryQuantizedVectorsFormat(name=ES818HnswBinaryQuantizedVectorsFormat, maxConn=" + m + ", beamWidth=" + efConstruction - + ", flatVectorFormat=ES816BinaryQuantizedVectorsFormat(" - + "name=ES816BinaryQuantizedVectorsFormat, " - + "flatVectorScorer=ES816BinaryFlatVectorsScorer(nonQuantizedDelegate=DefaultFlatVectorScorer())))"; + + ", flatVectorFormat=ES818BinaryQuantizedVectorsFormat(" + + "name=ES818BinaryQuantizedVectorsFormat, " + + "flatVectorScorer=ES818BinaryFlatVectorsScorer(nonQuantizedDelegate=DefaultFlatVectorScorer())))"; assertEquals(expectedString, knnVectorsFormat.toString()); } From 4b868b0e11f4a59d608523c57d1a62b870ac8e0e Mon Sep 17 00:00:00 2001 From: Niels Bauman <33722607+nielsbauman@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:57:40 +0100 Subject: [PATCH 24/39] Fix enrich cache size setting name (#117575) The enrich cache size setting accidentally got renamed from `enrich.cache_size` to `enrich.cache.size` in #111412. This commit updates the enrich plugin to accept both names and deprecates the wrong name. --- docs/changelog/117575.yaml | 5 ++ .../xpack/enrich/EnrichPlugin.java | 59 +++++++++++++++++- .../xpack/enrich/EnrichPluginTests.java | 62 +++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/117575.yaml create mode 100644 x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichPluginTests.java diff --git a/docs/changelog/117575.yaml b/docs/changelog/117575.yaml new file mode 100644 index 000000000000..781444ae97be --- /dev/null +++ b/docs/changelog/117575.yaml @@ -0,0 +1,5 @@ +pr: 117575 +summary: Fix enrich cache size setting name +area: Ingest Node +type: bug +issues: [] diff --git a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichPlugin.java b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichPlugin.java index 1a68ada60b6f..d46639d70042 100644 --- a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichPlugin.java +++ b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichPlugin.java @@ -14,6 +14,8 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.logging.DeprecationCategory; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Setting; @@ -23,6 +25,7 @@ import org.elasticsearch.common.unit.MemorySizeValue; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.core.UpdateForV10; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.ingest.Processor; @@ -74,6 +77,8 @@ public class EnrichPlugin extends Plugin implements SystemIndexPlugin, IngestPlugin { + private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(EnrichPlugin.class); + static final Setting ENRICH_FETCH_SIZE_SETTING = Setting.intSetting( "enrich.fetch_size", 10000, @@ -126,9 +131,9 @@ public class EnrichPlugin extends Plugin implements SystemIndexPlugin, IngestPlu return String.valueOf(maxConcurrentRequests * maxLookupsPerRequest); }, val -> Setting.parseInt(val, 1, Integer.MAX_VALUE, QUEUE_CAPACITY_SETTING_NAME), Setting.Property.NodeScope); - public static final String CACHE_SIZE_SETTING_NAME = "enrich.cache.size"; + public static final String CACHE_SIZE_SETTING_NAME = "enrich.cache_size"; public static final Setting CACHE_SIZE = new Setting<>( - "enrich.cache.size", + CACHE_SIZE_SETTING_NAME, (String) null, (String s) -> FlatNumberOrByteSizeValue.parse( s, @@ -138,16 +143,59 @@ public class EnrichPlugin extends Plugin implements SystemIndexPlugin, IngestPlu Setting.Property.NodeScope ); + /** + * This setting solely exists because the original setting was accidentally renamed in + * https://github.com/elastic/elasticsearch/pull/111412. + */ + @UpdateForV10(owner = UpdateForV10.Owner.DATA_MANAGEMENT) + public static final String CACHE_SIZE_SETTING_BWC_NAME = "enrich.cache.size"; + public static final Setting CACHE_SIZE_BWC = new Setting<>( + CACHE_SIZE_SETTING_BWC_NAME, + (String) null, + (String s) -> FlatNumberOrByteSizeValue.parse( + s, + CACHE_SIZE_SETTING_BWC_NAME, + new FlatNumberOrByteSizeValue(ByteSizeValue.ofBytes((long) (0.01 * JvmInfo.jvmInfo().getConfiguredMaxHeapSize()))) + ), + Setting.Property.NodeScope, + Setting.Property.Deprecated + ); + private final Settings settings; private final EnrichCache enrichCache; + private final long maxCacheSize; public EnrichPlugin(final Settings settings) { this.settings = settings; - FlatNumberOrByteSizeValue maxSize = CACHE_SIZE.get(settings); + FlatNumberOrByteSizeValue maxSize; + if (settings.hasValue(CACHE_SIZE_SETTING_BWC_NAME)) { + if (settings.hasValue(CACHE_SIZE_SETTING_NAME)) { + throw new IllegalArgumentException( + Strings.format( + "Both [{}] and [{}] are set, please use [{}]", + CACHE_SIZE_SETTING_NAME, + CACHE_SIZE_SETTING_BWC_NAME, + CACHE_SIZE_SETTING_NAME + ) + ); + } + deprecationLogger.warn( + DeprecationCategory.SETTINGS, + "enrich_cache_size_name", + "The [{}] setting is deprecated and will be removed in a future version. Please use [{}] instead.", + CACHE_SIZE_SETTING_BWC_NAME, + CACHE_SIZE_SETTING_NAME + ); + maxSize = CACHE_SIZE_BWC.get(settings); + } else { + maxSize = CACHE_SIZE.get(settings); + } if (maxSize.byteSizeValue() != null) { this.enrichCache = new EnrichCache(maxSize.byteSizeValue()); + this.maxCacheSize = maxSize.byteSizeValue().getBytes(); } else { this.enrichCache = new EnrichCache(maxSize.flatNumber()); + this.maxCacheSize = maxSize.flatNumber(); } } @@ -286,6 +334,11 @@ public String getFeatureDescription() { return "Manages data related to Enrich policies"; } + // Visible for testing + long getMaxCacheSize() { + return maxCacheSize; + } + /** * A class that specifies either a flat (unit-less) number or a byte size value. */ diff --git a/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichPluginTests.java b/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichPluginTests.java new file mode 100644 index 000000000000..07de0e096744 --- /dev/null +++ b/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichPluginTests.java @@ -0,0 +1,62 @@ +/* + * 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.enrich; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import java.util.List; + +public class EnrichPluginTests extends ESTestCase { + + public void testConstructWithByteSize() { + final var size = randomNonNegativeInt(); + Settings settings = Settings.builder().put(EnrichPlugin.CACHE_SIZE_SETTING_NAME, size + "b").build(); + EnrichPlugin plugin = new EnrichPlugin(settings); + assertEquals(size, plugin.getMaxCacheSize()); + } + + public void testConstructWithFlatNumber() { + final var size = randomNonNegativeInt(); + Settings settings = Settings.builder().put(EnrichPlugin.CACHE_SIZE_SETTING_NAME, size).build(); + EnrichPlugin plugin = new EnrichPlugin(settings); + assertEquals(size, plugin.getMaxCacheSize()); + } + + public void testConstructWithByteSizeBwc() { + final var size = randomNonNegativeInt(); + Settings settings = Settings.builder().put(EnrichPlugin.CACHE_SIZE_SETTING_BWC_NAME, size + "b").build(); + EnrichPlugin plugin = new EnrichPlugin(settings); + assertEquals(size, plugin.getMaxCacheSize()); + } + + public void testConstructWithFlatNumberBwc() { + final var size = randomNonNegativeInt(); + Settings settings = Settings.builder().put(EnrichPlugin.CACHE_SIZE_SETTING_BWC_NAME, size).build(); + EnrichPlugin plugin = new EnrichPlugin(settings); + assertEquals(size, plugin.getMaxCacheSize()); + } + + public void testConstructWithBothSettings() { + Settings settings = Settings.builder() + .put(EnrichPlugin.CACHE_SIZE_SETTING_NAME, randomNonNegativeInt()) + .put(EnrichPlugin.CACHE_SIZE_SETTING_BWC_NAME, randomNonNegativeInt()) + .build(); + assertThrows(IllegalArgumentException.class, () -> new EnrichPlugin(settings)); + } + + @Override + protected List filteredWarnings() { + final var warnings = super.filteredWarnings(); + warnings.add("[enrich.cache.size] setting was deprecated in Elasticsearch and will be removed in a future release."); + warnings.add( + "The [enrich.cache.size] setting is deprecated and will be removed in a future version. Please use [enrich.cache_size] instead." + ); + return warnings; + } +} From dcae87da47f258f041e164b4d5620b1adf7be090 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:48:29 -0500 Subject: [PATCH 25/39] [ML] Fixing failing inference api streaming tests (#118284) * Fixing tests with alphanumeric strings * Removing prints --- muted-tests.yml | 6 ------ .../org/elasticsearch/test/ESTestCase.java | 20 +++++++++++++++++++ .../test/cluster/FeatureFlag.java | 3 ++- .../inference/InferenceBaseRestTest.java | 2 ++ .../xpack/inference/InferenceCrudIT.java | 4 ++-- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index f922a1a27b62..d356dd2f791d 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -186,9 +186,6 @@ tests: - class: "org.elasticsearch.xpack.esql.qa.mixed.MixedClusterEsqlSpecIT" method: "test {scoring.*}" issue: https://github.com/elastic/elasticsearch/issues/117641 -- class: org.elasticsearch.xpack.inference.InferenceCrudIT - method: testSupportedStream - issue: https://github.com/elastic/elasticsearch/issues/117745 - class: org.elasticsearch.xpack.esql.ccq.MultiClusterSpecIT method: test {scoring.QstrWithFieldAndScoringSortedEval} issue: https://github.com/elastic/elasticsearch/issues/117751 @@ -272,9 +269,6 @@ tests: - class: org.elasticsearch.packaging.test.ArchiveTests method: test51AutoConfigurationWithPasswordProtectedKeystore issue: https://github.com/elastic/elasticsearch/issues/118212 -- class: org.elasticsearch.xpack.inference.InferenceCrudIT - method: testUnifiedCompletionInference - issue: https://github.com/elastic/elasticsearch/issues/118210 - class: org.elasticsearch.ingest.common.IngestCommonClientYamlTestSuiteIT issue: https://github.com/elastic/elasticsearch/issues/118215 - class: org.elasticsearch.datastreams.DataStreamsClientYamlTestSuiteIT diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index a71f61740e17..e869fc0836ba 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -277,6 +277,11 @@ public static void resetPortCounter() { private static final SetOnce WARN_SECURE_RANDOM_FIPS_NOT_DETERMINISTIC = new SetOnce<>(); + private static final String LOWER_ALPHA_CHARACTERS = "abcdefghijklmnopqrstuvwxyz"; + private static final String UPPER_ALPHA_CHARACTERS = LOWER_ALPHA_CHARACTERS.toUpperCase(Locale.ROOT); + private static final String DIGIT_CHARACTERS = "0123456789"; + private static final String ALPHANUMERIC_CHARACTERS = LOWER_ALPHA_CHARACTERS + UPPER_ALPHA_CHARACTERS + DIGIT_CHARACTERS; + static { Random random = initTestSeed(); TEST_WORKER_VM_ID = System.getProperty(TEST_WORKER_SYS_PROPERTY, DEFAULT_TEST_WORKER_ID); @@ -1200,6 +1205,21 @@ public static String randomAlphaOfLength(int codeUnits) { return RandomizedTest.randomAsciiOfLength(codeUnits); } + /** + * Generate a random string containing only alphanumeric characters. + * @param length the length of the string to generate + * @return the generated string + */ + public static String randomAlphanumericOfLength(int length) { + StringBuilder sb = new StringBuilder(); + Random random = random(); + for (int i = 0; i < length; i++) { + sb.append(ALPHANUMERIC_CHARACTERS.charAt(random.nextInt(ALPHANUMERIC_CHARACTERS.length()))); + } + + return sb.toString(); + } + public static SecureString randomSecureStringOfLength(int codeUnits) { var randomAlpha = randomAlphaOfLength(codeUnits); return new SecureString(randomAlpha.toCharArray()); diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java index 11787866af0d..5630c33ad559 100644 --- a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java @@ -18,7 +18,8 @@ public enum FeatureFlag { TIME_SERIES_MODE("es.index_mode_feature_flag_registered=true", Version.fromString("8.0.0"), null), FAILURE_STORE_ENABLED("es.failure_store_feature_flag_enabled=true", Version.fromString("8.12.0"), null), - SUB_OBJECTS_AUTO_ENABLED("es.sub_objects_auto_feature_flag_enabled=true", Version.fromString("8.16.0"), null); + SUB_OBJECTS_AUTO_ENABLED("es.sub_objects_auto_feature_flag_enabled=true", Version.fromString("8.16.0"), null), + INFERENCE_UNIFIED_API_ENABLED("es.inference_unified_feature_flag_enabled=true", Version.fromString("8.18.0"), null); public final String systemProperty; public final Version from; diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java index 07ce2fe00642..5b7394e89bc4 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java @@ -19,6 +19,7 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.inference.TaskType; import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.FeatureFlag; import org.elasticsearch.test.cluster.local.distribution.DistributionType; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.XContentBuilder; @@ -46,6 +47,7 @@ public class InferenceBaseRestTest extends ESRestTestCase { .setting("xpack.security.enabled", "true") .plugin("inference-service-test") .user("x_pack_rest_user", "x-pack-test-password") + .feature(FeatureFlag.INFERENCE_UNIFIED_API_ENABLED) .build(); @Override diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java index 1e19491aeaa6..da1d10db4da8 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java @@ -466,7 +466,7 @@ public void testSupportedStream() throws Exception { assertEquals(modelId, singleModel.get("inference_id")); assertEquals(TaskType.COMPLETION.toString(), singleModel.get("task_type")); - var input = IntStream.range(1, 2 + randomInt(8)).mapToObj(i -> randomUUID()).toList(); + var input = IntStream.range(1, 2 + randomInt(8)).mapToObj(i -> randomAlphanumericOfLength(5)).toList(); try { var events = streamInferOnMockService(modelId, TaskType.COMPLETION, input); @@ -493,7 +493,7 @@ public void testUnifiedCompletionInference() throws Exception { assertEquals(modelId, singleModel.get("inference_id")); assertEquals(TaskType.COMPLETION.toString(), singleModel.get("task_type")); - var input = IntStream.range(1, 2 + randomInt(8)).mapToObj(i -> randomUUID()).toList(); + var input = IntStream.range(1, 2 + randomInt(8)).mapToObj(i -> randomAlphanumericOfLength(5)).toList(); try { var events = unifiedCompletionInferOnMockService(modelId, TaskType.COMPLETION, input); var expectedResponses = expectedResultsIterator(input); From 31678a377df0681c0fca6d45675174df04f0b7e3 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 9 Dec 2024 13:35:21 -0500 Subject: [PATCH 26/39] Rename multi-dense vector to rank vectors (#118183) renames `multi_dense_vector` field mapper and such to `rank_vectors` to better describe its restricted usage. --- .../org.elasticsearch.script.fields.txt | 10 +- .../org.elasticsearch.script.score.txt | 4 +- .../painless/org.elasticsearch.txt | 2 +- ...x_sim.yml => 141_rank_vectors_max_sim.yml} | 10 +- ...yml => 181_rank_vectors_dv_fields_api.yml} | 10 +- ...i_dense_vector.yml => 30_rank_vectors.yml} | 14 +-- ...a.java => RankVectorsDVLeafFieldData.java} | 18 +-- ...apper.java => RankVectorsFieldMapper.java} | 47 ++++--- ...ta.java => RankVectorsIndexFieldData.java} | 14 +-- ...s.java => RankVectorsScriptDocValues.java} | 24 ++-- .../mapper/vectors/VectorEncoderDecoder.java | 10 -- .../elasticsearch/indices/IndicesModule.java | 6 +- .../action/search/SearchCapabilities.java | 22 ++-- ....java => RankVectorsScoreScriptUtils.java} | 50 ++++---- ...tiDenseVector.java => BitRankVectors.java} | 4 +- ...java => BitRankVectorsDocValuesField.java} | 14 +-- ...iDenseVector.java => ByteRankVectors.java} | 4 +- ...ava => ByteRankVectorsDocValuesField.java} | 22 ++-- ...DenseVector.java => FloatRankVectors.java} | 4 +- ...va => FloatRankVectorsDocValuesField.java} | 22 ++-- ...MultiDenseVector.java => RankVectors.java} | 8 +- ...ld.java => RankVectorsDocValuesField.java} | 20 +-- ....java => RankVectorsFieldMapperTests.java} | 65 +++++----- ...ts.java => RankVectorsFieldTypeTests.java} | 48 ++++---- ...a => RankVectorsScriptDocValuesTests.java} | 116 +++++------------- ... => RankVectorsScoreScriptUtilsTests.java} | 84 ++++++------- ...VectorTests.java => RankVectorsTests.java} | 18 +-- .../aggregations/AggregatorTestCase.java | 4 +- 28 files changed, 295 insertions(+), 379 deletions(-) rename modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/{141_multi_dense_vector_max_sim.yml => 141_rank_vectors_max_sim.yml} (95%) rename modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/{181_multi_dense_vector_dv_fields_api.yml => 181_rank_vectors_dv_fields_api.yml} (94%) rename rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/{30_multi_dense_vector.yml => 30_rank_vectors.yml} (88%) rename server/src/main/java/org/elasticsearch/index/mapper/vectors/{MultiVectorDVLeafFieldData.java => RankVectorsDVLeafFieldData.java} (70%) rename server/src/main/java/org/elasticsearch/index/mapper/vectors/{MultiDenseVectorFieldMapper.java => RankVectorsFieldMapper.java} (90%) rename server/src/main/java/org/elasticsearch/index/mapper/vectors/{MultiVectorIndexFieldData.java => RankVectorsIndexFieldData.java} (87%) rename server/src/main/java/org/elasticsearch/index/mapper/vectors/{MultiDenseVectorScriptDocValues.java => RankVectorsScriptDocValues.java} (70%) rename server/src/main/java/org/elasticsearch/script/{MultiVectorScoreScriptUtils.java => RankVectorsScoreScriptUtils.java} (85%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{BitMultiDenseVector.java => BitRankVectors.java} (94%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{BitMultiDenseVectorDocValuesField.java => BitRankVectorsDocValuesField.java} (64%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{ByteMultiDenseVector.java => ByteRankVectors.java} (95%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{ByteMultiDenseVectorDocValuesField.java => ByteRankVectorsDocValuesField.java} (85%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{FloatMultiDenseVector.java => FloatRankVectors.java} (93%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{FloatMultiDenseVectorDocValuesField.java => FloatRankVectorsDocValuesField.java} (85%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{MultiDenseVector.java => RankVectors.java} (92%) rename server/src/main/java/org/elasticsearch/script/field/vectors/{MultiDenseVectorDocValuesField.java => RankVectorsDocValuesField.java} (68%) rename server/src/test/java/org/elasticsearch/index/mapper/vectors/{MultiDenseVectorFieldMapperTests.java => RankVectorsFieldMapperTests.java} (87%) rename server/src/test/java/org/elasticsearch/index/mapper/vectors/{MultiDenseVectorFieldTypeTests.java => RankVectorsFieldTypeTests.java} (62%) rename server/src/test/java/org/elasticsearch/index/mapper/vectors/{MultiDenseVectorScriptDocValuesTests.java => RankVectorsScriptDocValuesTests.java} (75%) rename server/src/test/java/org/elasticsearch/script/{MultiVectorScoreScriptUtilsTests.java => RankVectorsScoreScriptUtilsTests.java} (81%) rename server/src/test/java/org/elasticsearch/script/field/vectors/{MultiDenseVectorTests.java => RankVectorsTests.java} (80%) diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt index 875b9a1dac3e..85dba97a392b 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt @@ -132,8 +132,8 @@ class org.elasticsearch.script.field.SeqNoDocValuesField @dynamic_type { class org.elasticsearch.script.field.VersionDocValuesField @dynamic_type { } -class org.elasticsearch.script.field.vectors.MultiDenseVector { - MultiDenseVector EMPTY +class org.elasticsearch.script.field.vectors.RankVectors { + RankVectors EMPTY float[] getMagnitudes() Iterator getVectors() @@ -142,9 +142,9 @@ class org.elasticsearch.script.field.vectors.MultiDenseVector { int size() } -class org.elasticsearch.script.field.vectors.MultiDenseVectorDocValuesField { - MultiDenseVector get() - MultiDenseVector get(MultiDenseVector) +class org.elasticsearch.script.field.vectors.RankVectorsDocValuesField { + RankVectors get() + RankVectors get(RankVectors) } class org.elasticsearch.script.field.vectors.DenseVector { diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.score.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.score.txt index 5a1d8c002aa1..a5118db4876c 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.score.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.score.txt @@ -50,7 +50,7 @@ static_import { double cosineSimilarity(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.VectorScoreScriptUtils$CosineSimilarity double dotProduct(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.VectorScoreScriptUtils$DotProduct double hamming(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.VectorScoreScriptUtils$Hamming - double maxSimDotProduct(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.MultiVectorScoreScriptUtils$MaxSimDotProduct - double maxSimInvHamming(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.MultiVectorScoreScriptUtils$MaxSimInvHamming + double maxSimDotProduct(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.RankVectorsScoreScriptUtils$MaxSimDotProduct + double maxSimInvHamming(org.elasticsearch.script.ScoreScript, Object, String) bound_to org.elasticsearch.script.RankVectorsScoreScriptUtils$MaxSimInvHamming } diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt index b2db0d1006d4..4815b9c10e73 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt @@ -123,7 +123,7 @@ class org.elasticsearch.index.mapper.vectors.DenseVectorScriptDocValues { float getMagnitude() } -class org.elasticsearch.index.mapper.vectors.MultiDenseVectorScriptDocValues { +class org.elasticsearch.index.mapper.vectors.RankVectorsScriptDocValues { Iterator getVectorValues() float[] getMagnitudes() } diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/141_multi_dense_vector_max_sim.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/141_rank_vectors_max_sim.yml similarity index 95% rename from modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/141_multi_dense_vector_max_sim.yml rename to modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/141_rank_vectors_max_sim.yml index 77d4b70cdfca..7c46fbc9a26a 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/141_multi_dense_vector_max_sim.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/141_rank_vectors_max_sim.yml @@ -3,9 +3,9 @@ setup: capabilities: - method: POST path: /_search - capabilities: [ multi_dense_vector_script_max_sim_with_bugfix ] + capabilities: [ rank_vectors_script_max_sim_with_bugfix ] test_runner_features: capabilities - reason: "Support for multi dense vector max-sim functions capability required" + reason: "Support for rank vectors max-sim functions capability required" - skip: features: headers @@ -18,14 +18,14 @@ setup: mappings: properties: vector: - type: multi_dense_vector + type: rank_vectors dims: 5 byte_vector: - type: multi_dense_vector + type: rank_vectors dims: 5 element_type: byte bit_vector: - type: multi_dense_vector + type: rank_vectors dims: 40 element_type: bit - do: diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/181_multi_dense_vector_dv_fields_api.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/181_rank_vectors_dv_fields_api.yml similarity index 94% rename from modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/181_multi_dense_vector_dv_fields_api.yml rename to modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/181_rank_vectors_dv_fields_api.yml index 66cb3f3c46fc..f37e554fca7b 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/181_multi_dense_vector_dv_fields_api.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/181_rank_vectors_dv_fields_api.yml @@ -3,9 +3,9 @@ setup: capabilities: - method: POST path: /_search - capabilities: [ multi_dense_vector_script_access ] + capabilities: [ rank_vectors_script_access ] test_runner_features: capabilities - reason: "Support for multi dense vector field script access capability required" + reason: "Support for rank vector field script access capability required" - skip: features: headers @@ -18,14 +18,14 @@ setup: mappings: properties: vector: - type: multi_dense_vector + type: rank_vectors dims: 5 byte_vector: - type: multi_dense_vector + type: rank_vectors dims: 5 element_type: byte bit_vector: - type: multi_dense_vector + type: rank_vectors dims: 40 element_type: bit - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/30_multi_dense_vector.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/30_rank_vectors.yml similarity index 88% rename from rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/30_multi_dense_vector.yml rename to rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/30_rank_vectors.yml index 80d1d25dfcbd..ecf34f46c338 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/30_multi_dense_vector.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/30_rank_vectors.yml @@ -3,9 +3,9 @@ setup: capabilities: - method: POST path: /_search - capabilities: [ multi_dense_vector_field_mapper ] + capabilities: [ rank_vectors_field_mapper ] test_runner_features: capabilities - reason: "Support for multi dense vector field mapper capability required" + reason: "Support for rank vectors field mapper capability required" --- "Test create multi-vector field": - do: @@ -15,7 +15,7 @@ setup: mappings: properties: vector1: - type: multi_dense_vector + type: rank_vectors dims: 3 - do: index: @@ -48,7 +48,7 @@ setup: name: type: keyword vector1: - type: multi_dense_vector + type: rank_vectors - do: index: index: test @@ -88,7 +88,7 @@ setup: mappings: properties: vector1: - type: multi_dense_vector + type: rank_vectors - do: catch: bad_request index: @@ -105,7 +105,7 @@ setup: mappings: properties: vector1: - type: multi_dense_vector + type: rank_vectors dims: 3 - do: catch: bad_request @@ -123,7 +123,7 @@ setup: mappings: properties: vector1: - type: multi_dense_vector + type: rank_vectors dims: 3 - do: catch: bad_request diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiVectorDVLeafFieldData.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsDVLeafFieldData.java similarity index 70% rename from server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiVectorDVLeafFieldData.java rename to server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsDVLeafFieldData.java index b9716d315f33..0125d0249ec2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiVectorDVLeafFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsDVLeafFieldData.java @@ -15,19 +15,19 @@ import org.elasticsearch.index.fielddata.LeafFieldData; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.script.field.DocValuesScriptFieldFactory; -import org.elasticsearch.script.field.vectors.BitMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.ByteMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.FloatMultiDenseVectorDocValuesField; +import org.elasticsearch.script.field.vectors.BitRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.ByteRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.FloatRankVectorsDocValuesField; import java.io.IOException; -final class MultiVectorDVLeafFieldData implements LeafFieldData { +final class RankVectorsDVLeafFieldData implements LeafFieldData { private final LeafReader reader; private final String field; private final DenseVectorFieldMapper.ElementType elementType; private final int dims; - MultiVectorDVLeafFieldData(LeafReader reader, String field, DenseVectorFieldMapper.ElementType elementType, int dims) { + RankVectorsDVLeafFieldData(LeafReader reader, String field, DenseVectorFieldMapper.ElementType elementType, int dims) { this.reader = reader; this.field = field; this.elementType = elementType; @@ -38,11 +38,11 @@ final class MultiVectorDVLeafFieldData implements LeafFieldData { public DocValuesScriptFieldFactory getScriptFieldFactory(String name) { try { BinaryDocValues values = DocValues.getBinary(reader, field); - BinaryDocValues magnitudeValues = DocValues.getBinary(reader, field + MultiDenseVectorFieldMapper.VECTOR_MAGNITUDES_SUFFIX); + BinaryDocValues magnitudeValues = DocValues.getBinary(reader, field + RankVectorsFieldMapper.VECTOR_MAGNITUDES_SUFFIX); return switch (elementType) { - case BYTE -> new ByteMultiDenseVectorDocValuesField(values, magnitudeValues, name, elementType, dims); - case FLOAT -> new FloatMultiDenseVectorDocValuesField(values, magnitudeValues, name, elementType, dims); - case BIT -> new BitMultiDenseVectorDocValuesField(values, magnitudeValues, name, elementType, dims); + case BYTE -> new ByteRankVectorsDocValuesField(values, magnitudeValues, name, elementType, dims); + case FLOAT -> new FloatRankVectorsDocValuesField(values, magnitudeValues, name, elementType, dims); + case BIT -> new BitRankVectorsDocValuesField(values, magnitudeValues, name, elementType, dims); }; } catch (IOException e) { throw new IllegalStateException("Cannot load doc values for multi-vector field!", e); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldMapper.java similarity index 90% rename from server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldMapper.java rename to server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldMapper.java index b23a1f1f6679..d57dbf79b450 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldMapper.java @@ -51,14 +51,14 @@ import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.MAX_DIMS_COUNT_BIT; import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.namesToElementType; -public class MultiDenseVectorFieldMapper extends FieldMapper { +public class RankVectorsFieldMapper extends FieldMapper { public static final String VECTOR_MAGNITUDES_SUFFIX = "._magnitude"; - public static final FeatureFlag FEATURE_FLAG = new FeatureFlag("multi_dense_vector"); - public static final String CONTENT_TYPE = "multi_dense_vector"; + public static final FeatureFlag FEATURE_FLAG = new FeatureFlag("rank_vectors"); + public static final String CONTENT_TYPE = "rank_vectors"; - private static MultiDenseVectorFieldMapper toType(FieldMapper in) { - return (MultiDenseVectorFieldMapper) in; + private static RankVectorsFieldMapper toType(FieldMapper in) { + return (RankVectorsFieldMapper) in; } public static class Builder extends FieldMapper.Builder { @@ -122,24 +122,24 @@ protected Parameter[] getParameters() { return new Parameter[] { elementType, dims, meta }; } - public MultiDenseVectorFieldMapper.Builder dimensions(int dimensions) { + public RankVectorsFieldMapper.Builder dimensions(int dimensions) { this.dims.setValue(dimensions); return this; } - public MultiDenseVectorFieldMapper.Builder elementType(DenseVectorFieldMapper.ElementType elementType) { + public RankVectorsFieldMapper.Builder elementType(DenseVectorFieldMapper.ElementType elementType) { this.elementType.setValue(elementType); return this; } @Override - public MultiDenseVectorFieldMapper build(MapperBuilderContext context) { + public RankVectorsFieldMapper build(MapperBuilderContext context) { // Validate again here because the dimensions or element type could have been set programmatically, // which affects index option validity validate(); - return new MultiDenseVectorFieldMapper( + return new RankVectorsFieldMapper( leafName(), - new MultiDenseVectorFieldType( + new RankVectorsFieldType( context.buildFullName(leafName()), elementType.getValue(), dims.getValue(), @@ -153,16 +153,16 @@ public MultiDenseVectorFieldMapper build(MapperBuilderContext context) { } public static final TypeParser PARSER = new TypeParser( - (n, c) -> new MultiDenseVectorFieldMapper.Builder(n, c.indexVersionCreated()), + (n, c) -> new RankVectorsFieldMapper.Builder(n, c.indexVersionCreated()), notInMultiFields(CONTENT_TYPE) ); - public static final class MultiDenseVectorFieldType extends SimpleMappedFieldType { + public static final class RankVectorsFieldType extends SimpleMappedFieldType { private final DenseVectorFieldMapper.ElementType elementType; private final Integer dims; private final IndexVersion indexCreatedVersion; - public MultiDenseVectorFieldType( + public RankVectorsFieldType( String name, DenseVectorFieldMapper.ElementType elementType, Integer dims, @@ -207,7 +207,7 @@ public boolean isAggregatable() { @Override public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) { - return new MultiVectorIndexFieldData.Builder(name(), CoreValuesSourceType.KEYWORD, indexCreatedVersion, dims, elementType); + return new RankVectorsIndexFieldData.Builder(name(), CoreValuesSourceType.KEYWORD, indexCreatedVersion, dims, elementType); } @Override @@ -231,19 +231,14 @@ DenseVectorFieldMapper.ElementType getElementType() { private final IndexVersion indexCreatedVersion; - private MultiDenseVectorFieldMapper( - String simpleName, - MappedFieldType fieldType, - BuilderParams params, - IndexVersion indexCreatedVersion - ) { + private RankVectorsFieldMapper(String simpleName, MappedFieldType fieldType, BuilderParams params, IndexVersion indexCreatedVersion) { super(simpleName, fieldType, params); this.indexCreatedVersion = indexCreatedVersion; } @Override - public MultiDenseVectorFieldType fieldType() { - return (MultiDenseVectorFieldType) super.fieldType(); + public RankVectorsFieldType fieldType() { + return (RankVectorsFieldType) super.fieldType(); } @Override @@ -282,14 +277,14 @@ public void parse(DocumentParserContext context) throws IOException { ); } } - MultiDenseVectorFieldType updatedFieldType = new MultiDenseVectorFieldType( + RankVectorsFieldType updatedFieldType = new RankVectorsFieldType( fieldType().name(), fieldType().elementType, currentDims, indexCreatedVersion, fieldType().meta() ); - Mapper update = new MultiDenseVectorFieldMapper(leafName(), updatedFieldType, builderParams, indexCreatedVersion); + Mapper update = new RankVectorsFieldMapper(leafName(), updatedFieldType, builderParams, indexCreatedVersion); context.addDynamicMapper(update); return; } @@ -371,12 +366,12 @@ protected String contentType() { @Override public FieldMapper.Builder getMergeBuilder() { - return new MultiDenseVectorFieldMapper.Builder(leafName(), indexCreatedVersion).init(this); + return new RankVectorsFieldMapper.Builder(leafName(), indexCreatedVersion).init(this); } @Override protected SyntheticSourceSupport syntheticSourceSupport() { - return new SyntheticSourceSupport.Native(new MultiDenseVectorFieldMapper.DocValuesSyntheticFieldLoader()); + return new SyntheticSourceSupport.Native(new RankVectorsFieldMapper.DocValuesSyntheticFieldLoader()); } private class DocValuesSyntheticFieldLoader extends SourceLoader.DocValuesBasedSyntheticFieldLoader { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiVectorIndexFieldData.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsIndexFieldData.java similarity index 87% rename from server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiVectorIndexFieldData.java rename to server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsIndexFieldData.java index 44a666e25a61..7f54d2b9a8ad 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiVectorIndexFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsIndexFieldData.java @@ -22,14 +22,14 @@ import org.elasticsearch.search.sort.BucketedSort; import org.elasticsearch.search.sort.SortOrder; -public class MultiVectorIndexFieldData implements IndexFieldData { +public class RankVectorsIndexFieldData implements IndexFieldData { protected final String fieldName; protected final ValuesSourceType valuesSourceType; private final int dims; private final IndexVersion indexVersion; private final DenseVectorFieldMapper.ElementType elementType; - public MultiVectorIndexFieldData( + public RankVectorsIndexFieldData( String fieldName, int dims, ValuesSourceType valuesSourceType, @@ -54,19 +54,19 @@ public ValuesSourceType getValuesSourceType() { } @Override - public MultiVectorDVLeafFieldData load(LeafReaderContext context) { - return new MultiVectorDVLeafFieldData(context.reader(), fieldName, elementType, dims); + public RankVectorsDVLeafFieldData load(LeafReaderContext context) { + return new RankVectorsDVLeafFieldData(context.reader(), fieldName, elementType, dims); } @Override - public MultiVectorDVLeafFieldData loadDirect(LeafReaderContext context) throws Exception { + public RankVectorsDVLeafFieldData loadDirect(LeafReaderContext context) throws Exception { return load(context); } @Override public SortField sortField(Object missingValue, MultiValueMode sortMode, XFieldComparatorSource.Nested nested, boolean reverse) { throw new IllegalArgumentException( - "Field [" + fieldName + "] of type [" + MultiDenseVectorFieldMapper.CONTENT_TYPE + "] doesn't support sort" + "Field [" + fieldName + "] of type [" + RankVectorsFieldMapper.CONTENT_TYPE + "] doesn't support sort" ); } @@ -108,7 +108,7 @@ public Builder( @Override public IndexFieldData build(IndexFieldDataCache cache, CircuitBreakerService breakerService) { - return new MultiVectorIndexFieldData(name, dims, valuesSourceType, indexVersion, elementType); + return new RankVectorsIndexFieldData(name, dims, valuesSourceType, indexVersion, elementType); } } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorScriptDocValues.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsScriptDocValues.java similarity index 70% rename from server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorScriptDocValues.java rename to server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsScriptDocValues.java index a91960832239..e663df86c67c 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorScriptDocValues.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/RankVectorsScriptDocValues.java @@ -11,18 +11,18 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.index.fielddata.ScriptDocValues; -import org.elasticsearch.script.field.vectors.MultiDenseVector; +import org.elasticsearch.script.field.vectors.RankVectors; import java.util.Iterator; -public class MultiDenseVectorScriptDocValues extends ScriptDocValues { +public class RankVectorsScriptDocValues extends ScriptDocValues { - public static final String MISSING_VECTOR_FIELD_MESSAGE = "A document doesn't have a value for a multi-vector field!"; + public static final String MISSING_VECTOR_FIELD_MESSAGE = "A document doesn't have a value for a rank-vectors field!"; private final int dims; - protected final MultiDenseVectorSupplier dvSupplier; + protected final RankVectorsSupplier dvSupplier; - public MultiDenseVectorScriptDocValues(MultiDenseVectorSupplier supplier, int dims) { + public RankVectorsScriptDocValues(RankVectorsSupplier supplier, int dims) { super(supplier); this.dvSupplier = supplier; this.dims = dims; @@ -32,8 +32,8 @@ public int dims() { return dims; } - private MultiDenseVector getCheckedVector() { - MultiDenseVector vector = dvSupplier.getInternal(); + private RankVectors getCheckedVector() { + RankVectors vector = dvSupplier.getInternal(); if (vector == null) { throw new IllegalArgumentException(MISSING_VECTOR_FIELD_MESSAGE); } @@ -41,7 +41,7 @@ private MultiDenseVector getCheckedVector() { } /** - * Get multi-dense vector's value as an array of floats + * Get rank-vectors's value as an array of floats */ public Iterator getVectorValues() { return getCheckedVector().getVectors(); @@ -57,25 +57,25 @@ public float[] getMagnitudes() { @Override public BytesRef get(int index) { throw new UnsupportedOperationException( - "accessing a multi-vector field's value through 'get' or 'value' is not supported, use 'vectorValues' or 'magnitudes' instead." + "accessing a rank-vectors field's value through 'get' or 'value' is not supported, use 'vectorValues' or 'magnitudes' instead." ); } @Override public int size() { - MultiDenseVector mdv = dvSupplier.getInternal(); + RankVectors mdv = dvSupplier.getInternal(); if (mdv != null) { return mdv.size(); } return 0; } - public interface MultiDenseVectorSupplier extends Supplier { + public interface RankVectorsSupplier extends Supplier { @Override default BytesRef getInternal(int index) { throw new UnsupportedOperationException(); } - MultiDenseVector getInternal(); + RankVectors getInternal(); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorEncoderDecoder.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorEncoderDecoder.java index 3db2d164846b..54b369ab1f37 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorEncoderDecoder.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorEncoderDecoder.java @@ -94,14 +94,4 @@ public static float[] getMultiMagnitudes(BytesRef magnitudes) { return multiMagnitudes; } - public static void decodeMultiDenseVector(BytesRef vectorBR, int numVectors, float[][] multiVectorValue) { - if (vectorBR == null) { - throw new IllegalArgumentException(MultiDenseVectorScriptDocValues.MISSING_VECTOR_FIELD_MESSAGE); - } - FloatBuffer fb = ByteBuffer.wrap(vectorBR.bytes, vectorBR.offset, vectorBR.length).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer(); - for (int i = 0; i < numVectors; i++) { - fb.get(multiVectorValue[i]); - } - } - } diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index 340bff4e1c85..3dc25b058b1d 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -67,7 +67,7 @@ import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.mapper.flattened.FlattenedFieldMapper; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorFieldMapper; +import org.elasticsearch.index.mapper.vectors.RankVectorsFieldMapper; import org.elasticsearch.index.mapper.vectors.SparseVectorFieldMapper; import org.elasticsearch.index.seqno.RetentionLeaseBackgroundSyncAction; import org.elasticsearch.index.seqno.RetentionLeaseSyncAction; @@ -211,8 +211,8 @@ public static Map getMappers(List mappe mappers.put(DenseVectorFieldMapper.CONTENT_TYPE, DenseVectorFieldMapper.PARSER); mappers.put(SparseVectorFieldMapper.CONTENT_TYPE, SparseVectorFieldMapper.PARSER); - if (MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()) { - mappers.put(MultiDenseVectorFieldMapper.CONTENT_TYPE, MultiDenseVectorFieldMapper.PARSER); + if (RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()) { + mappers.put(RankVectorsFieldMapper.CONTENT_TYPE, RankVectorsFieldMapper.PARSER); } for (MapperPlugin mapperPlugin : mapperPlugins) { diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java b/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java index 57980321bdc3..c9d9569abe93 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java @@ -10,7 +10,7 @@ package org.elasticsearch.rest.action.search; import org.elasticsearch.Build; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorFieldMapper; +import org.elasticsearch.index.mapper.vectors.RankVectorsFieldMapper; import java.util.HashSet; import java.util.Set; @@ -34,14 +34,14 @@ private SearchCapabilities() {} private static final String TRANSFORM_RANK_RRF_TO_RETRIEVER = "transform_rank_rrf_to_retriever"; /** Support kql query. */ private static final String KQL_QUERY_SUPPORTED = "kql_query"; - /** Support multi-dense-vector field mapper. */ - private static final String MULTI_DENSE_VECTOR_FIELD_MAPPER = "multi_dense_vector_field_mapper"; + /** Support rank-vectors field mapper. */ + private static final String RANK_VECTORS_FIELD_MAPPER = "rank_vectors_field_mapper"; /** Support propagating nested retrievers' inner_hits to top-level compound retrievers . */ private static final String NESTED_RETRIEVER_INNER_HITS_SUPPORT = "nested_retriever_inner_hits_support"; - /** Support multi-dense-vector script field access. */ - private static final String MULTI_DENSE_VECTOR_SCRIPT_ACCESS = "multi_dense_vector_script_access"; - /** Initial support for multi-dense-vector maxSim functions access. */ - private static final String MULTI_DENSE_VECTOR_SCRIPT_MAX_SIM = "multi_dense_vector_script_max_sim_with_bugfix"; + /** Support rank-vectors script field access. */ + private static final String RANK_VECTORS_SCRIPT_ACCESS = "rank_vectors_script_access"; + /** Initial support for rank-vectors maxSim functions access. */ + private static final String RANK_VECTORS_SCRIPT_MAX_SIM = "rank_vectors_script_max_sim_with_bugfix"; private static final String RANDOM_SAMPLER_WITH_SCORED_SUBAGGS = "random_sampler_with_scored_subaggs"; private static final String OPTIMIZED_SCALAR_QUANTIZATION_BBQ = "optimized_scalar_quantization_bbq"; @@ -57,10 +57,10 @@ private SearchCapabilities() {} capabilities.add(NESTED_RETRIEVER_INNER_HITS_SUPPORT); capabilities.add(RANDOM_SAMPLER_WITH_SCORED_SUBAGGS); capabilities.add(OPTIMIZED_SCALAR_QUANTIZATION_BBQ); - if (MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()) { - capabilities.add(MULTI_DENSE_VECTOR_FIELD_MAPPER); - capabilities.add(MULTI_DENSE_VECTOR_SCRIPT_ACCESS); - capabilities.add(MULTI_DENSE_VECTOR_SCRIPT_MAX_SIM); + if (RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()) { + capabilities.add(RANK_VECTORS_FIELD_MAPPER); + capabilities.add(RANK_VECTORS_SCRIPT_ACCESS); + capabilities.add(RANK_VECTORS_SCRIPT_MAX_SIM); } if (Build.current().isSnapshot()) { capabilities.add(KQL_QUERY_SUPPORTED); diff --git a/server/src/main/java/org/elasticsearch/script/MultiVectorScoreScriptUtils.java b/server/src/main/java/org/elasticsearch/script/RankVectorsScoreScriptUtils.java similarity index 85% rename from server/src/main/java/org/elasticsearch/script/MultiVectorScoreScriptUtils.java rename to server/src/main/java/org/elasticsearch/script/RankVectorsScoreScriptUtils.java index 136c5e7b57d4..2d11641cb5aa 100644 --- a/server/src/main/java/org/elasticsearch/script/MultiVectorScoreScriptUtils.java +++ b/server/src/main/java/org/elasticsearch/script/RankVectorsScoreScriptUtils.java @@ -12,19 +12,19 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; import org.elasticsearch.script.field.vectors.DenseVector; -import org.elasticsearch.script.field.vectors.MultiDenseVectorDocValuesField; +import org.elasticsearch.script.field.vectors.RankVectorsDocValuesField; import java.io.IOException; import java.util.HexFormat; import java.util.List; -public class MultiVectorScoreScriptUtils { +public class RankVectorsScoreScriptUtils { - public static class MultiDenseVectorFunction { + public static class RankVectorsFunction { protected final ScoreScript scoreScript; - protected final MultiDenseVectorDocValuesField field; + protected final RankVectorsDocValuesField field; - public MultiDenseVectorFunction(ScoreScript scoreScript, MultiDenseVectorDocValuesField field) { + public RankVectorsFunction(ScoreScript scoreScript, RankVectorsDocValuesField field) { this.scoreScript = scoreScript; this.field = field; } @@ -41,7 +41,7 @@ void setNextVector() { } } - public static class ByteMultiDenseVectorFunction extends MultiDenseVectorFunction { + public static class ByteRankVectorsFunction extends RankVectorsFunction { protected final byte[][] queryVector; /** @@ -51,7 +51,7 @@ public static class ByteMultiDenseVectorFunction extends MultiDenseVectorFunctio * @param field The vector field. * @param queryVector The query vector. */ - public ByteMultiDenseVectorFunction(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, List> queryVector) { + public ByteRankVectorsFunction(ScoreScript scoreScript, RankVectorsDocValuesField field, List> queryVector) { super(scoreScript, field); if (queryVector.isEmpty()) { throw new IllegalArgumentException("The query vector is empty."); @@ -84,13 +84,13 @@ public ByteMultiDenseVectorFunction(ScoreScript scoreScript, MultiDenseVectorDoc * @param field The vector field. * @param queryVector The query vector. */ - public ByteMultiDenseVectorFunction(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, byte[][] queryVector) { + public ByteRankVectorsFunction(ScoreScript scoreScript, RankVectorsDocValuesField field, byte[][] queryVector) { super(scoreScript, field); this.queryVector = queryVector; } } - public static class FloatMultiDenseVectorFunction extends MultiDenseVectorFunction { + public static class FloatRankVectorsFunction extends RankVectorsFunction { protected final float[][] queryVector; /** @@ -100,11 +100,7 @@ public static class FloatMultiDenseVectorFunction extends MultiDenseVectorFuncti * @param field The vector field. * @param queryVector The query vector. */ - public FloatMultiDenseVectorFunction( - ScoreScript scoreScript, - MultiDenseVectorDocValuesField field, - List> queryVector - ) { + public FloatRankVectorsFunction(ScoreScript scoreScript, RankVectorsDocValuesField field, List> queryVector) { super(scoreScript, field); if (queryVector.isEmpty()) { throw new IllegalArgumentException("The query vector is empty."); @@ -133,13 +129,13 @@ public interface MaxSimInvHammingDistanceInterface { float maxSimInvHamming(); } - public static class ByteMaxSimInvHammingDistance extends ByteMultiDenseVectorFunction implements MaxSimInvHammingDistanceInterface { + public static class ByteMaxSimInvHammingDistance extends ByteRankVectorsFunction implements MaxSimInvHammingDistanceInterface { - public ByteMaxSimInvHammingDistance(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, List> queryVector) { + public ByteMaxSimInvHammingDistance(ScoreScript scoreScript, RankVectorsDocValuesField field, List> queryVector) { super(scoreScript, field, queryVector); } - public ByteMaxSimInvHammingDistance(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, byte[][] queryVector) { + public ByteMaxSimInvHammingDistance(ScoreScript scoreScript, RankVectorsDocValuesField field, byte[][] queryVector) { super(scoreScript, field, queryVector); } @@ -183,7 +179,7 @@ public static final class MaxSimInvHamming { private final MaxSimInvHammingDistanceInterface function; public MaxSimInvHamming(ScoreScript scoreScript, Object queryVector, String fieldName) { - MultiDenseVectorDocValuesField field = (MultiDenseVectorDocValuesField) scoreScript.field(fieldName); + RankVectorsDocValuesField field = (RankVectorsDocValuesField) scoreScript.field(fieldName); if (field.getElementType() == DenseVectorFieldMapper.ElementType.FLOAT) { throw new IllegalArgumentException("hamming distance is only supported for byte or bit vectors"); } @@ -205,11 +201,11 @@ public interface MaxSimDotProductInterface { double maxSimDotProduct(); } - public static class MaxSimBitDotProduct extends MultiDenseVectorFunction implements MaxSimDotProductInterface { + public static class MaxSimBitDotProduct extends RankVectorsFunction implements MaxSimDotProductInterface { private final byte[][] byteQueryVector; private final float[][] floatQueryVector; - public MaxSimBitDotProduct(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, byte[][] queryVector) { + public MaxSimBitDotProduct(ScoreScript scoreScript, RankVectorsDocValuesField field, byte[][] queryVector) { super(scoreScript, field); if (field.getElementType() != DenseVectorFieldMapper.ElementType.BIT) { throw new IllegalArgumentException("Cannot calculate bit dot product for non-bit vectors"); @@ -230,7 +226,7 @@ public MaxSimBitDotProduct(ScoreScript scoreScript, MultiDenseVectorDocValuesFie this.floatQueryVector = null; } - public MaxSimBitDotProduct(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, List> queryVector) { + public MaxSimBitDotProduct(ScoreScript scoreScript, RankVectorsDocValuesField field, List> queryVector) { super(scoreScript, field); if (queryVector.isEmpty()) { throw new IllegalArgumentException("The query vector is empty."); @@ -304,13 +300,13 @@ public double maxSimDotProduct() { } } - public static class MaxSimByteDotProduct extends ByteMultiDenseVectorFunction implements MaxSimDotProductInterface { + public static class MaxSimByteDotProduct extends ByteRankVectorsFunction implements MaxSimDotProductInterface { - public MaxSimByteDotProduct(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, List> queryVector) { + public MaxSimByteDotProduct(ScoreScript scoreScript, RankVectorsDocValuesField field, List> queryVector) { super(scoreScript, field, queryVector); } - public MaxSimByteDotProduct(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, byte[][] queryVector) { + public MaxSimByteDotProduct(ScoreScript scoreScript, RankVectorsDocValuesField field, byte[][] queryVector) { super(scoreScript, field, queryVector); } @@ -320,9 +316,9 @@ public double maxSimDotProduct() { } } - public static class MaxSimFloatDotProduct extends FloatMultiDenseVectorFunction implements MaxSimDotProductInterface { + public static class MaxSimFloatDotProduct extends FloatRankVectorsFunction implements MaxSimDotProductInterface { - public MaxSimFloatDotProduct(ScoreScript scoreScript, MultiDenseVectorDocValuesField field, List> queryVector) { + public MaxSimFloatDotProduct(ScoreScript scoreScript, RankVectorsDocValuesField field, List> queryVector) { super(scoreScript, field, queryVector); } @@ -338,7 +334,7 @@ public static final class MaxSimDotProduct { @SuppressWarnings("unchecked") public MaxSimDotProduct(ScoreScript scoreScript, Object queryVector, String fieldName) { - MultiDenseVectorDocValuesField field = (MultiDenseVectorDocValuesField) scoreScript.field(fieldName); + RankVectorsDocValuesField field = (RankVectorsDocValuesField) scoreScript.field(fieldName); function = switch (field.getElementType()) { case BIT -> { BytesOrList bytesOrList = parseBytes(queryVector); diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/BitMultiDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/BitRankVectors.java similarity index 94% rename from server/src/main/java/org/elasticsearch/script/field/vectors/BitMultiDenseVector.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/BitRankVectors.java index 7805816090d5..0e2984c2a7df 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/BitMultiDenseVector.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/BitRankVectors.java @@ -15,8 +15,8 @@ import java.util.Arrays; -public class BitMultiDenseVector extends ByteMultiDenseVector { - public BitMultiDenseVector(VectorIterator vectorValues, BytesRef magnitudesBytes, int numVecs, int dims) { +public class BitRankVectors extends ByteRankVectors { + public BitRankVectors(VectorIterator vectorValues, BytesRef magnitudesBytes, int numVecs, int dims) { super(vectorValues, magnitudesBytes, numVecs, dims); } diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/BitMultiDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/BitRankVectorsDocValuesField.java similarity index 64% rename from server/src/main/java/org/elasticsearch/script/field/vectors/BitMultiDenseVectorDocValuesField.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/BitRankVectorsDocValuesField.java index 35a43eabb8f0..6d38621440fb 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/BitMultiDenseVectorDocValuesField.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/BitRankVectorsDocValuesField.java @@ -12,20 +12,14 @@ import org.apache.lucene.index.BinaryDocValues; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType; -public class BitMultiDenseVectorDocValuesField extends ByteMultiDenseVectorDocValuesField { +public class BitRankVectorsDocValuesField extends ByteRankVectorsDocValuesField { - public BitMultiDenseVectorDocValuesField( - BinaryDocValues input, - BinaryDocValues magnitudes, - String name, - ElementType elementType, - int dims - ) { + public BitRankVectorsDocValuesField(BinaryDocValues input, BinaryDocValues magnitudes, String name, ElementType elementType, int dims) { super(input, magnitudes, name, elementType, dims / 8); } @Override - protected MultiDenseVector getVector() { - return new BitMultiDenseVector(vectorValue, magnitudesValue, numVecs, dims); + protected RankVectors getVector() { + return new BitRankVectors(vectorValue, magnitudesValue, numVecs, dims); } } diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteMultiDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteRankVectors.java similarity index 95% rename from server/src/main/java/org/elasticsearch/script/field/vectors/ByteMultiDenseVector.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/ByteRankVectors.java index 5e9d3e05746c..f8e82046037c 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteMultiDenseVector.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteRankVectors.java @@ -16,7 +16,7 @@ import java.util.Arrays; import java.util.Iterator; -public class ByteMultiDenseVector implements MultiDenseVector { +public class ByteRankVectors implements RankVectors { protected final VectorIterator vectorValues; protected final int numVecs; @@ -25,7 +25,7 @@ public class ByteMultiDenseVector implements MultiDenseVector { private float[] magnitudes; private final BytesRef magnitudesBytes; - public ByteMultiDenseVector(VectorIterator vectorValues, BytesRef magnitudesBytes, int numVecs, int dims) { + public ByteRankVectors(VectorIterator vectorValues, BytesRef magnitudesBytes, int numVecs, int dims) { assert magnitudesBytes.length == numVecs * Float.BYTES; this.vectorValues = vectorValues; this.numVecs = numVecs; diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteMultiDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteRankVectorsDocValuesField.java similarity index 85% rename from server/src/main/java/org/elasticsearch/script/field/vectors/ByteMultiDenseVectorDocValuesField.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/ByteRankVectorsDocValuesField.java index d45c5b85137f..db81bb6ebe1c 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteMultiDenseVectorDocValuesField.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteRankVectorsDocValuesField.java @@ -12,12 +12,12 @@ import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.util.BytesRef; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorScriptDocValues; +import org.elasticsearch.index.mapper.vectors.RankVectorsScriptDocValues; import java.io.IOException; import java.util.Iterator; -public class ByteMultiDenseVectorDocValuesField extends MultiDenseVectorDocValuesField { +public class ByteRankVectorsDocValuesField extends RankVectorsDocValuesField { protected final BinaryDocValues input; private final BinaryDocValues magnitudes; @@ -29,7 +29,7 @@ public class ByteMultiDenseVectorDocValuesField extends MultiDenseVectorDocValue protected BytesRef magnitudesValue; private byte[] buffer; - public ByteMultiDenseVectorDocValuesField( + public ByteRankVectorsDocValuesField( BinaryDocValues input, BinaryDocValues magnitudes, String name, @@ -63,25 +63,25 @@ public void setNextDocId(int docId) throws IOException { } @Override - public MultiDenseVectorScriptDocValues toScriptDocValues() { - return new MultiDenseVectorScriptDocValues(this, dims); + public RankVectorsScriptDocValues toScriptDocValues() { + return new RankVectorsScriptDocValues(this, dims); } - protected MultiDenseVector getVector() { - return new ByteMultiDenseVector(vectorValue, magnitudesValue, numVecs, dims); + protected RankVectors getVector() { + return new ByteRankVectors(vectorValue, magnitudesValue, numVecs, dims); } @Override - public MultiDenseVector get() { + public RankVectors get() { if (isEmpty()) { - return MultiDenseVector.EMPTY; + return RankVectors.EMPTY; } decodeVectorIfNecessary(); return getVector(); } @Override - public MultiDenseVector get(MultiDenseVector defaultValue) { + public RankVectors get(RankVectors defaultValue) { if (isEmpty()) { return defaultValue; } @@ -90,7 +90,7 @@ public MultiDenseVector get(MultiDenseVector defaultValue) { } @Override - public MultiDenseVector getInternal() { + public RankVectors getInternal() { return get(null); } diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/FloatMultiDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/FloatRankVectors.java similarity index 93% rename from server/src/main/java/org/elasticsearch/script/field/vectors/FloatMultiDenseVector.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/FloatRankVectors.java index 9c2f7eb6a86d..3ad5e53c047a 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/FloatMultiDenseVector.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/FloatRankVectors.java @@ -17,7 +17,7 @@ import static org.elasticsearch.index.mapper.vectors.VectorEncoderDecoder.getMultiMagnitudes; -public class FloatMultiDenseVector implements MultiDenseVector { +public class FloatRankVectors implements RankVectors { private final BytesRef magnitudes; private float[] magnitudesArray = null; @@ -25,7 +25,7 @@ public class FloatMultiDenseVector implements MultiDenseVector { private final int numVectors; private final VectorIterator vectorValues; - public FloatMultiDenseVector(VectorIterator decodedDocVector, BytesRef magnitudes, int numVectors, int dims) { + public FloatRankVectors(VectorIterator decodedDocVector, BytesRef magnitudes, int numVectors, int dims) { assert magnitudes.length == numVectors * Float.BYTES; this.vectorValues = decodedDocVector; this.magnitudes = magnitudes; diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/FloatMultiDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/FloatRankVectorsDocValuesField.java similarity index 85% rename from server/src/main/java/org/elasticsearch/script/field/vectors/FloatMultiDenseVectorDocValuesField.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/FloatRankVectorsDocValuesField.java index c7ac7842afd9..39bc1e621113 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/FloatMultiDenseVectorDocValuesField.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/FloatRankVectorsDocValuesField.java @@ -12,7 +12,7 @@ import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.util.BytesRef; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorScriptDocValues; +import org.elasticsearch.index.mapper.vectors.RankVectorsScriptDocValues; import java.io.IOException; import java.nio.ByteBuffer; @@ -20,7 +20,7 @@ import java.nio.FloatBuffer; import java.util.Iterator; -public class FloatMultiDenseVectorDocValuesField extends MultiDenseVectorDocValuesField { +public class FloatRankVectorsDocValuesField extends RankVectorsDocValuesField { private final BinaryDocValues input; private final BinaryDocValues magnitudes; @@ -32,7 +32,7 @@ public class FloatMultiDenseVectorDocValuesField extends MultiDenseVectorDocValu private int numVectors; private float[] buffer; - public FloatMultiDenseVectorDocValuesField( + public FloatRankVectorsDocValuesField( BinaryDocValues input, BinaryDocValues magnitudes, String name, @@ -66,8 +66,8 @@ public void setNextDocId(int docId) throws IOException { } @Override - public MultiDenseVectorScriptDocValues toScriptDocValues() { - return new MultiDenseVectorScriptDocValues(this, dims); + public RankVectorsScriptDocValues toScriptDocValues() { + return new RankVectorsScriptDocValues(this, dims); } @Override @@ -76,25 +76,25 @@ public boolean isEmpty() { } @Override - public MultiDenseVector get() { + public RankVectors get() { if (isEmpty()) { - return MultiDenseVector.EMPTY; + return RankVectors.EMPTY; } decodeVectorIfNecessary(); - return new FloatMultiDenseVector(vectorValues, magnitudesValue, numVectors, dims); + return new FloatRankVectors(vectorValues, magnitudesValue, numVectors, dims); } @Override - public MultiDenseVector get(MultiDenseVector defaultValue) { + public RankVectors get(RankVectors defaultValue) { if (isEmpty()) { return defaultValue; } decodeVectorIfNecessary(); - return new FloatMultiDenseVector(vectorValues, magnitudesValue, numVectors, dims); + return new FloatRankVectors(vectorValues, magnitudesValue, numVectors, dims); } @Override - public MultiDenseVector getInternal() { + public RankVectors getInternal() { return get(null); } diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/MultiDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/RankVectors.java similarity index 92% rename from server/src/main/java/org/elasticsearch/script/field/vectors/MultiDenseVector.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/RankVectors.java index 7d948cf5a74f..ec0157c2708c 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/MultiDenseVector.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/RankVectors.java @@ -11,7 +11,7 @@ import java.util.Iterator; -public interface MultiDenseVector { +public interface RankVectors { default void checkDimensions(int qvDims) { checkDimensions(getDims(), qvDims); @@ -45,9 +45,9 @@ private static String badQueryVectorType(Object queryVector) { return "Cannot use vector [" + queryVector + "] with class [" + queryVector.getClass().getName() + "] as query vector"; } - MultiDenseVector EMPTY = new MultiDenseVector() { - public static final String MISSING_VECTOR_FIELD_MESSAGE = "Multi Dense vector value missing for a field," - + " use isEmpty() to check for a missing vector value"; + RankVectors EMPTY = new RankVectors() { + public static final String MISSING_VECTOR_FIELD_MESSAGE = "rank-vectors value missing for a field," + + " use isEmpty() to check for a missing value"; @Override public Iterator getVectors() { diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/MultiDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/RankVectorsDocValuesField.java similarity index 68% rename from server/src/main/java/org/elasticsearch/script/field/vectors/MultiDenseVectorDocValuesField.java rename to server/src/main/java/org/elasticsearch/script/field/vectors/RankVectorsDocValuesField.java index 61ae4304683c..2362561ea88c 100644 --- a/server/src/main/java/org/elasticsearch/script/field/vectors/MultiDenseVectorDocValuesField.java +++ b/server/src/main/java/org/elasticsearch/script/field/vectors/RankVectorsDocValuesField.java @@ -9,7 +9,7 @@ package org.elasticsearch.script.field.vectors; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorScriptDocValues; +import org.elasticsearch.index.mapper.vectors.RankVectorsScriptDocValues; import org.elasticsearch.script.field.AbstractScriptFieldFactory; import org.elasticsearch.script.field.DocValuesScriptFieldFactory; import org.elasticsearch.script.field.Field; @@ -18,15 +18,15 @@ import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType; -public abstract class MultiDenseVectorDocValuesField extends AbstractScriptFieldFactory +public abstract class RankVectorsDocValuesField extends AbstractScriptFieldFactory implements - Field, + Field, DocValuesScriptFieldFactory, - MultiDenseVectorScriptDocValues.MultiDenseVectorSupplier { + RankVectorsScriptDocValues.RankVectorsSupplier { protected final String name; protected final ElementType elementType; - public MultiDenseVectorDocValuesField(String name, ElementType elementType) { + public RankVectorsDocValuesField(String name, ElementType elementType) { this.name = name; this.elementType = elementType; } @@ -43,15 +43,15 @@ public ElementType getElementType() { /** * Get the DenseVector for a document if one exists, DenseVector.EMPTY otherwise */ - public abstract MultiDenseVector get(); + public abstract RankVectors get(); - public abstract MultiDenseVector get(MultiDenseVector defaultValue); + public abstract RankVectors get(RankVectors defaultValue); - public abstract MultiDenseVectorScriptDocValues toScriptDocValues(); + public abstract RankVectorsScriptDocValues toScriptDocValues(); // DenseVector fields are single valued, so Iterable does not make sense. @Override - public Iterator iterator() { - throw new UnsupportedOperationException("Cannot iterate over single valued multi_dense_vector field, use get() instead"); + public Iterator iterator() { + throw new UnsupportedOperationException("Cannot iterate over single valued rank_vectors field, use get() instead"); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldMapperTests.java similarity index 87% rename from server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldMapperTests.java rename to server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldMapperTests.java index 6a890328732c..e81c28cbcc44 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldMapperTests.java @@ -51,17 +51,17 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class MultiDenseVectorFieldMapperTests extends MapperTestCase { +public class RankVectorsFieldMapperTests extends MapperTestCase { @BeforeClass public static void setup() { - assumeTrue("Requires multi-dense vector support", MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()); + assumeTrue("Requires rank vectors support", RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()); } private final ElementType elementType; private final int dims; - public MultiDenseVectorFieldMapperTests() { + public RankVectorsFieldMapperTests() { this.elementType = randomFrom(ElementType.BYTE, ElementType.FLOAT, ElementType.BIT); this.dims = ElementType.BIT == elementType ? 4 * Byte.SIZE : 4; } @@ -77,7 +77,7 @@ protected void minimalMapping(XContentBuilder b, IndexVersion indexVersion) thro } private void indexMapping(XContentBuilder b, IndexVersion indexVersion) throws IOException { - b.field("type", "multi_dense_vector").field("dims", dims); + b.field("type", "rank_vectors").field("dims", dims); if (elementType != ElementType.FLOAT) { b.field("element_type", elementType.toString()); } @@ -95,23 +95,23 @@ protected Object getSampleValueForDocument() { protected void registerParameters(ParameterChecker checker) throws IOException { checker.registerConflictCheck( "dims", - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims)), - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims + 8)) + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims)), + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims + 8)) ); checker.registerConflictCheck( "element_type", - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims).field("element_type", "byte")), - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims).field("element_type", "float")) + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims).field("element_type", "byte")), + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims).field("element_type", "float")) ); checker.registerConflictCheck( "element_type", - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims).field("element_type", "float")), - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims * 8).field("element_type", "bit")) + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims).field("element_type", "float")), + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims * 8).field("element_type", "bit")) ); checker.registerConflictCheck( "element_type", - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims).field("element_type", "byte")), - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", dims * 8).field("element_type", "bit")) + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims).field("element_type", "byte")), + fieldMapping(b -> b.field("type", "rank_vectors").field("dims", dims * 8).field("element_type", "bit")) ); } @@ -127,7 +127,7 @@ protected boolean supportsIgnoreMalformed() { @Override protected void assertSearchable(MappedFieldType fieldType) { - assertThat(fieldType, instanceOf(MultiDenseVectorFieldMapper.MultiDenseVectorFieldType.class)); + assertThat(fieldType, instanceOf(RankVectorsFieldMapper.RankVectorsFieldType.class)); assertFalse(fieldType.isIndexed()); assertFalse(fieldType.isSearchable()); } @@ -147,7 +147,7 @@ public void testAggregatableConsistency() {} public void testDims() { { Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> { - b.field("type", "multi_dense_vector"); + b.field("type", "rank_vectors"); b.field("dims", 0); }))); assertThat( @@ -158,7 +158,7 @@ public void testDims() { // test max limit for non-indexed vectors { Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> { - b.field("type", "multi_dense_vector"); + b.field("type", "rank_vectors"); b.field("dims", 5000); }))); assertThat( @@ -171,14 +171,14 @@ public void testDims() { public void testMergeDims() throws IOException { XContentBuilder mapping = mapping(b -> { b.startObject("field"); - b.field("type", "multi_dense_vector"); + b.field("type", "rank_vectors"); b.endObject(); }); MapperService mapperService = createMapperService(mapping); mapping = mapping(b -> { b.startObject("field"); - b.field("type", "multi_dense_vector").field("dims", dims); + b.field("type", "rank_vectors").field("dims", dims); b.endObject(); }); merge(mapperService, mapping); @@ -190,14 +190,14 @@ public void testMergeDims() throws IOException { public void testLargeDimsBit() throws IOException { createMapperService(fieldMapping(b -> { - b.field("type", "multi_dense_vector"); + b.field("type", "rank_vectors"); b.field("dims", 1024 * Byte.SIZE); b.field("element_type", ElementType.BIT.toString()); })); } public void testNonIndexedVector() throws Exception { - DocumentMapper mapper = createDocumentMapper(fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", 3))); + DocumentMapper mapper = createDocumentMapper(fieldMapping(b -> b.field("type", "rank_vectors").field("dims", 3))); float[][] validVectors = { { -12.1f, 100.7f, -4 }, { 42f, .05f, -1f } }; double[] dotProduct = new double[2]; @@ -234,7 +234,7 @@ public void testNonIndexedVector() throws Exception { .asFloatBuffer(); fb.get(decodedValues[i]); } - List magFields = doc1.rootDoc().getFields("field" + MultiDenseVectorFieldMapper.VECTOR_MAGNITUDES_SUFFIX); + List magFields = doc1.rootDoc().getFields("field" + RankVectorsFieldMapper.VECTOR_MAGNITUDES_SUFFIX); assertEquals(1, magFields.size()); assertThat(magFields.get(0), instanceOf(BinaryDocValuesField.class)); BytesRef magBR = magFields.get(0).binaryValue(); @@ -249,7 +249,7 @@ public void testNonIndexedVector() throws Exception { } public void testPoorlyIndexedVector() throws Exception { - DocumentMapper mapper = createDocumentMapper(fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", 3))); + DocumentMapper mapper = createDocumentMapper(fieldMapping(b -> b.field("type", "rank_vectors").field("dims", 3))); float[][] validVectors = { { -12.1f, 100.7f, -4 }, { 42f, .05f, -1f } }; double[] dotProduct = new double[2]; @@ -279,27 +279,23 @@ public void testInvalidParameters() { MapperParsingException e = expectThrows( MapperParsingException.class, - () -> createDocumentMapper( - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", 3).field("element_type", "foo")) - ) + () -> createDocumentMapper(fieldMapping(b -> b.field("type", "rank_vectors").field("dims", 3).field("element_type", "foo"))) ); assertThat(e.getMessage(), containsString("invalid element_type [foo]; available types are ")); e = expectThrows( MapperParsingException.class, - () -> createDocumentMapper( - fieldMapping(b -> b.field("type", "multi_dense_vector").field("dims", 3).startObject("foo").endObject()) - ) + () -> createDocumentMapper(fieldMapping(b -> b.field("type", "rank_vectors").field("dims", 3).startObject("foo").endObject())) ); assertThat( e.getMessage(), - containsString("Failed to parse mapping: unknown parameter [foo] on mapper [field] of type [multi_dense_vector]") + containsString("Failed to parse mapping: unknown parameter [foo] on mapper [field] of type [rank_vectors]") ); } public void testDocumentsWithIncorrectDims() throws Exception { int dims = 3; XContentBuilder fieldMapping = fieldMapping(b -> { - b.field("type", "multi_dense_vector"); + b.field("type", "rank_vectors"); b.field("dims", dims); }); @@ -382,8 +378,7 @@ protected void assertFetch(MapperService mapperService, String field, Object val Source s = SourceProvider.fromStoredFields().getSource(ir.leaves().get(0), 0); nativeFetcher.setNextReader(ir.leaves().get(0)); List fromNative = nativeFetcher.fetchValues(s, 0, new ArrayList<>()); - MultiDenseVectorFieldMapper.MultiDenseVectorFieldType denseVectorFieldType = - (MultiDenseVectorFieldMapper.MultiDenseVectorFieldType) ft; + RankVectorsFieldMapper.RankVectorsFieldType denseVectorFieldType = (RankVectorsFieldMapper.RankVectorsFieldType) ft; switch (denseVectorFieldType.getElementType()) { case BYTE -> assumeFalse("byte element type testing not currently added", false); case FLOAT -> { @@ -408,12 +403,12 @@ protected void assertFetch(MapperService mapperService, String field, Object val @Override protected void randomFetchTestFieldConfig(XContentBuilder b) throws IOException { - b.field("type", "multi_dense_vector").field("dims", randomIntBetween(2, 4096)).field("element_type", "float"); + b.field("type", "rank_vectors").field("dims", randomIntBetween(2, 4096)).field("element_type", "float"); } @Override protected Object generateRandomInputValue(MappedFieldType ft) { - MultiDenseVectorFieldMapper.MultiDenseVectorFieldType vectorFieldType = (MultiDenseVectorFieldMapper.MultiDenseVectorFieldType) ft; + RankVectorsFieldMapper.RankVectorsFieldType vectorFieldType = (RankVectorsFieldMapper.RankVectorsFieldType) ft; int numVectors = randomIntBetween(1, 16); return switch (vectorFieldType.getElementType()) { case BYTE -> { @@ -451,7 +446,7 @@ public void testCannotBeUsedInMultifields() { b.endObject(); b.endObject(); }))); - assertThat(e.getMessage(), containsString("Field [vectors] of type [multi_dense_vector] can't be used in multifields")); + assertThat(e.getMessage(), containsString("Field [vectors] of type [rank_vectors] can't be used in multifields")); } @Override @@ -486,7 +481,7 @@ public SyntheticSourceExample example(int maxValues) { } private void mapping(XContentBuilder b) throws IOException { - b.field("type", "multi_dense_vector"); + b.field("type", "rank_vectors"); if (elementType == ElementType.BYTE || elementType == ElementType.BIT || randomBoolean()) { b.field("element_type", elementType.toString()); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldTypeTests.java similarity index 62% rename from server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldTypeTests.java rename to server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldTypeTests.java index 14cc63e31fa2..b4cbbc4730d7 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsFieldTypeTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.index.fielddata.FieldDataContext; import org.elasticsearch.index.mapper.FieldTypeTestCase; import org.elasticsearch.index.mapper.MappedFieldType; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorFieldMapper.MultiDenseVectorFieldType; +import org.elasticsearch.index.mapper.vectors.RankVectorsFieldMapper.RankVectorsFieldType; import org.junit.BeforeClass; import java.io.IOException; @@ -23,15 +23,15 @@ import static org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.BBQ_MIN_DIMS; -public class MultiDenseVectorFieldTypeTests extends FieldTypeTestCase { +public class RankVectorsFieldTypeTests extends FieldTypeTestCase { @BeforeClass public static void setup() { - assumeTrue("Requires multi-dense vector support", MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()); + assumeTrue("Requires rank-vectors support", RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()); } - private MultiDenseVectorFieldType createFloatFieldType() { - return new MultiDenseVectorFieldType( + private RankVectorsFieldType createFloatFieldType() { + return new RankVectorsFieldType( "f", DenseVectorFieldMapper.ElementType.FLOAT, BBQ_MIN_DIMS, @@ -40,66 +40,60 @@ private MultiDenseVectorFieldType createFloatFieldType() { ); } - private MultiDenseVectorFieldType createByteFieldType() { - return new MultiDenseVectorFieldType( - "f", - DenseVectorFieldMapper.ElementType.BYTE, - 5, - IndexVersion.current(), - Collections.emptyMap() - ); + private RankVectorsFieldType createByteFieldType() { + return new RankVectorsFieldType("f", DenseVectorFieldMapper.ElementType.BYTE, 5, IndexVersion.current(), Collections.emptyMap()); } public void testHasDocValues() { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); assertTrue(fft.hasDocValues()); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); assertTrue(bft.hasDocValues()); } public void testIsIndexed() { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); assertFalse(fft.isIndexed()); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); assertFalse(bft.isIndexed()); } public void testIsSearchable() { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); assertFalse(fft.isSearchable()); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); assertFalse(bft.isSearchable()); } public void testIsAggregatable() { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); assertFalse(fft.isAggregatable()); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); assertFalse(bft.isAggregatable()); } public void testFielddataBuilder() { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); FieldDataContext fdc = new FieldDataContext("test", null, () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT); assertNotNull(fft.fielddataBuilder(fdc)); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); FieldDataContext bdc = new FieldDataContext("test", null, () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT); assertNotNull(bft.fielddataBuilder(bdc)); } public void testDocValueFormat() { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); expectThrows(IllegalArgumentException.class, () -> fft.docValueFormat(null, null)); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); expectThrows(IllegalArgumentException.class, () -> bft.docValueFormat(null, null)); } public void testFetchSourceValue() throws IOException { - MultiDenseVectorFieldType fft = createFloatFieldType(); + RankVectorsFieldType fft = createFloatFieldType(); List> vector = List.of(List.of(0.0, 1.0, 2.0, 3.0, 4.0, 6.0)); assertEquals(vector, fetchSourceValue(fft, vector)); - MultiDenseVectorFieldType bft = createByteFieldType(); + RankVectorsFieldType bft = createByteFieldType(); assertEquals(vector, fetchSourceValue(bft, vector)); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorScriptDocValuesTests.java b/server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsScriptDocValuesTests.java similarity index 75% rename from server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorScriptDocValuesTests.java rename to server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsScriptDocValuesTests.java index 435baa477e74..c38ed0f60f0a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/vectors/MultiDenseVectorScriptDocValuesTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/vectors/RankVectorsScriptDocValuesTests.java @@ -13,10 +13,10 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType; -import org.elasticsearch.script.field.vectors.ByteMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.FloatMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.MultiDenseVector; -import org.elasticsearch.script.field.vectors.MultiDenseVectorDocValuesField; +import org.elasticsearch.script.field.vectors.ByteRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.FloatRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.RankVectors; +import org.elasticsearch.script.field.vectors.RankVectorsDocValuesField; import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; @@ -27,11 +27,11 @@ import static org.hamcrest.Matchers.containsString; -public class MultiDenseVectorScriptDocValuesTests extends ESTestCase { +public class RankVectorsScriptDocValuesTests extends ESTestCase { @BeforeClass public static void setup() { - assumeTrue("Requires multi-dense vector support", MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()); + assumeTrue("Requires rank-vectors support", RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()); } public void testFloatGetVectorValueAndGetMagnitude() throws IOException { @@ -41,14 +41,8 @@ public void testFloatGetVectorValueAndGetMagnitude() throws IOException { BinaryDocValues docValues = wrap(vectors, ElementType.FLOAT); BinaryDocValues magnitudeValues = wrap(expectedMagnitudes); - MultiDenseVectorDocValuesField field = new FloatMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.FLOAT, - dims - ); - MultiDenseVectorScriptDocValues scriptDocValues = field.toScriptDocValues(); + RankVectorsDocValuesField field = new FloatRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.FLOAT, dims); + RankVectorsScriptDocValues scriptDocValues = field.toScriptDocValues(); for (int i = 0; i < vectors.length; i++) { field.setNextDocId(i); assertEquals(vectors[i].length, field.size()); @@ -71,14 +65,8 @@ public void testByteGetVectorValueAndGetMagnitude() throws IOException { BinaryDocValues docValues = wrap(vectors, ElementType.BYTE); BinaryDocValues magnitudeValues = wrap(expectedMagnitudes); - MultiDenseVectorDocValuesField field = new ByteMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.BYTE, - dims - ); - MultiDenseVectorScriptDocValues scriptDocValues = field.toScriptDocValues(); + RankVectorsDocValuesField field = new ByteRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.BYTE, dims); + RankVectorsScriptDocValues scriptDocValues = field.toScriptDocValues(); for (int i = 0; i < vectors.length; i++) { field.setNextDocId(i); assertEquals(vectors[i].length, field.size()); @@ -101,25 +89,19 @@ public void testFloatMetadataAndIterator() throws IOException { BinaryDocValues docValues = wrap(vectors, ElementType.FLOAT); BinaryDocValues magnitudeValues = wrap(magnitudes); - MultiDenseVectorDocValuesField field = new FloatMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.FLOAT, - dims - ); + RankVectorsDocValuesField field = new FloatRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.FLOAT, dims); for (int i = 0; i < vectors.length; i++) { field.setNextDocId(i); - MultiDenseVector dv = field.get(); + RankVectors dv = field.get(); assertEquals(vectors[i].length, dv.size()); assertFalse(dv.isEmpty()); assertEquals(dims, dv.getDims()); UnsupportedOperationException e = expectThrows(UnsupportedOperationException.class, field::iterator); - assertEquals("Cannot iterate over single valued multi_dense_vector field, use get() instead", e.getMessage()); + assertEquals("Cannot iterate over single valued rank_vectors field, use get() instead", e.getMessage()); } field.setNextDocId(vectors.length); - MultiDenseVector dv = field.get(); - assertEquals(dv, MultiDenseVector.EMPTY); + RankVectors dv = field.get(); + assertEquals(dv, RankVectors.EMPTY); } public void testByteMetadataAndIterator() throws IOException { @@ -128,25 +110,19 @@ public void testByteMetadataAndIterator() throws IOException { float[][] magnitudes = new float[][] { new float[3], new float[2] }; BinaryDocValues docValues = wrap(vectors, ElementType.BYTE); BinaryDocValues magnitudeValues = wrap(magnitudes); - MultiDenseVectorDocValuesField field = new ByteMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.BYTE, - dims - ); + RankVectorsDocValuesField field = new ByteRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.BYTE, dims); for (int i = 0; i < vectors.length; i++) { field.setNextDocId(i); - MultiDenseVector dv = field.get(); + RankVectors dv = field.get(); assertEquals(vectors[i].length, dv.size()); assertFalse(dv.isEmpty()); assertEquals(dims, dv.getDims()); UnsupportedOperationException e = expectThrows(UnsupportedOperationException.class, field::iterator); - assertEquals("Cannot iterate over single valued multi_dense_vector field, use get() instead", e.getMessage()); + assertEquals("Cannot iterate over single valued rank_vectors field, use get() instead", e.getMessage()); } field.setNextDocId(vectors.length); - MultiDenseVector dv = field.get(); - assertEquals(dv, MultiDenseVector.EMPTY); + RankVectors dv = field.get(); + assertEquals(dv, RankVectors.EMPTY); } protected float[][] fill(float[][] vectors, ElementType elementType) { @@ -164,22 +140,16 @@ public void testFloatMissingValues() throws IOException { float[][] magnitudes = { { 1.7320f, 2.4495f, 3.3166f }, { 2.2361f } }; BinaryDocValues docValues = wrap(vectors, ElementType.FLOAT); BinaryDocValues magnitudeValues = wrap(magnitudes); - MultiDenseVectorDocValuesField field = new FloatMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.FLOAT, - dims - ); - MultiDenseVectorScriptDocValues scriptDocValues = field.toScriptDocValues(); + RankVectorsDocValuesField field = new FloatRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.FLOAT, dims); + RankVectorsScriptDocValues scriptDocValues = field.toScriptDocValues(); field.setNextDocId(3); assertEquals(0, field.size()); Exception e = expectThrows(IllegalArgumentException.class, scriptDocValues::getVectorValues); - assertEquals("A document doesn't have a value for a multi-vector field!", e.getMessage()); + assertEquals("A document doesn't have a value for a rank-vectors field!", e.getMessage()); e = expectThrows(IllegalArgumentException.class, scriptDocValues::getMagnitudes); - assertEquals("A document doesn't have a value for a multi-vector field!", e.getMessage()); + assertEquals("A document doesn't have a value for a rank-vectors field!", e.getMessage()); } public void testByteMissingValues() throws IOException { @@ -188,22 +158,16 @@ public void testByteMissingValues() throws IOException { float[][] magnitudes = { { 1.7320f, 2.4495f, 3.3166f }, { 2.2361f } }; BinaryDocValues docValues = wrap(vectors, ElementType.BYTE); BinaryDocValues magnitudeValues = wrap(magnitudes); - MultiDenseVectorDocValuesField field = new ByteMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.BYTE, - dims - ); - MultiDenseVectorScriptDocValues scriptDocValues = field.toScriptDocValues(); + RankVectorsDocValuesField field = new ByteRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.BYTE, dims); + RankVectorsScriptDocValues scriptDocValues = field.toScriptDocValues(); field.setNextDocId(3); assertEquals(0, field.size()); Exception e = expectThrows(IllegalArgumentException.class, scriptDocValues::getVectorValues); - assertEquals("A document doesn't have a value for a multi-vector field!", e.getMessage()); + assertEquals("A document doesn't have a value for a rank-vectors field!", e.getMessage()); e = expectThrows(IllegalArgumentException.class, scriptDocValues::getMagnitudes); - assertEquals("A document doesn't have a value for a multi-vector field!", e.getMessage()); + assertEquals("A document doesn't have a value for a rank-vectors field!", e.getMessage()); } public void testFloatGetFunctionIsNotAccessible() throws IOException { @@ -212,21 +176,15 @@ public void testFloatGetFunctionIsNotAccessible() throws IOException { float[][] magnitudes = { { 1.7320f, 2.4495f, 3.3166f }, { 2.2361f } }; BinaryDocValues docValues = wrap(vectors, ElementType.FLOAT); BinaryDocValues magnitudeValues = wrap(magnitudes); - MultiDenseVectorDocValuesField field = new FloatMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.FLOAT, - dims - ); - MultiDenseVectorScriptDocValues scriptDocValues = field.toScriptDocValues(); + RankVectorsDocValuesField field = new FloatRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.FLOAT, dims); + RankVectorsScriptDocValues scriptDocValues = field.toScriptDocValues(); field.setNextDocId(0); Exception e = expectThrows(UnsupportedOperationException.class, () -> scriptDocValues.get(0)); assertThat( e.getMessage(), containsString( - "accessing a multi-vector field's value through 'get' or 'value' is not supported," + "accessing a rank-vectors field's value through 'get' or 'value' is not supported," + " use 'vectorValues' or 'magnitudes' instead." ) ); @@ -238,21 +196,15 @@ public void testByteGetFunctionIsNotAccessible() throws IOException { float[][] magnitudes = { { 1.7320f, 2.4495f, 3.3166f }, { 2.2361f } }; BinaryDocValues docValues = wrap(vectors, ElementType.BYTE); BinaryDocValues magnitudeValues = wrap(magnitudes); - MultiDenseVectorDocValuesField field = new ByteMultiDenseVectorDocValuesField( - docValues, - magnitudeValues, - "test", - ElementType.BYTE, - dims - ); - MultiDenseVectorScriptDocValues scriptDocValues = field.toScriptDocValues(); + RankVectorsDocValuesField field = new ByteRankVectorsDocValuesField(docValues, magnitudeValues, "test", ElementType.BYTE, dims); + RankVectorsScriptDocValues scriptDocValues = field.toScriptDocValues(); field.setNextDocId(0); Exception e = expectThrows(UnsupportedOperationException.class, () -> scriptDocValues.get(0)); assertThat( e.getMessage(), containsString( - "accessing a multi-vector field's value through 'get' or 'value' is not supported," + "accessing a rank-vectors field's value through 'get' or 'value' is not supported," + " use 'vectorValues' or 'magnitudes' instead." ) ); diff --git a/server/src/test/java/org/elasticsearch/script/MultiVectorScoreScriptUtilsTests.java b/server/src/test/java/org/elasticsearch/script/RankVectorsScoreScriptUtilsTests.java similarity index 81% rename from server/src/test/java/org/elasticsearch/script/MultiVectorScoreScriptUtilsTests.java rename to server/src/test/java/org/elasticsearch/script/RankVectorsScoreScriptUtilsTests.java index f908f5117047..917cc2069a29 100644 --- a/server/src/test/java/org/elasticsearch/script/MultiVectorScoreScriptUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/script/RankVectorsScoreScriptUtilsTests.java @@ -11,14 +11,14 @@ import org.apache.lucene.util.VectorUtil; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorFieldMapper; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorScriptDocValuesTests; -import org.elasticsearch.script.MultiVectorScoreScriptUtils.MaxSimDotProduct; -import org.elasticsearch.script.MultiVectorScoreScriptUtils.MaxSimInvHamming; -import org.elasticsearch.script.field.vectors.BitMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.ByteMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.FloatMultiDenseVectorDocValuesField; -import org.elasticsearch.script.field.vectors.MultiDenseVectorDocValuesField; +import org.elasticsearch.index.mapper.vectors.RankVectorsFieldMapper; +import org.elasticsearch.index.mapper.vectors.RankVectorsScriptDocValuesTests; +import org.elasticsearch.script.RankVectorsScoreScriptUtils.MaxSimDotProduct; +import org.elasticsearch.script.RankVectorsScoreScriptUtils.MaxSimInvHamming; +import org.elasticsearch.script.field.vectors.BitRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.ByteRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.FloatRankVectorsDocValuesField; +import org.elasticsearch.script.field.vectors.RankVectorsDocValuesField; import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; @@ -31,11 +31,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class MultiVectorScoreScriptUtilsTests extends ESTestCase { +public class RankVectorsScoreScriptUtilsTests extends ESTestCase { @BeforeClass public static void setup() { - assumeTrue("Requires multi-dense vector support", MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()); + assumeTrue("Requires rank-vectors support", RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()); } public void testFloatMultiVectorClassBindings() throws IOException { @@ -53,23 +53,23 @@ public void testFloatMultiVectorClassBindings() throws IOException { List> queryVector = List.of(Arrays.asList(0.5f, 111.3f, -13.0f, 14.8f, -156.0f)); List> invalidQueryVector = List.of(Arrays.asList(0.5, 111.3)); - List fields = List.of( - new FloatMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(docVectors, ElementType.FLOAT), - MultiDenseVectorScriptDocValuesTests.wrap(docMagnitudes), + List fields = List.of( + new FloatRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(docVectors, ElementType.FLOAT), + RankVectorsScriptDocValuesTests.wrap(docMagnitudes), "test", ElementType.FLOAT, dims ), - new FloatMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(docVectors, ElementType.FLOAT), - MultiDenseVectorScriptDocValuesTests.wrap(docMagnitudes), + new FloatRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(docVectors, ElementType.FLOAT), + RankVectorsScriptDocValuesTests.wrap(docMagnitudes), "test", ElementType.FLOAT, dims ) ); - for (MultiDenseVectorDocValuesField field : fields) { + for (RankVectorsDocValuesField field : fields) { field.setNextDocId(0); ScoreScript scoreScript = mock(ScoreScript.class); @@ -88,7 +88,7 @@ public void testFloatMultiVectorClassBindings() throws IOException { // Check each function rejects query vectors with the wrong dimension IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> new MultiVectorScoreScriptUtils.MaxSimDotProduct(scoreScript, invalidQueryVector, fieldName) + () -> new RankVectorsScoreScriptUtils.MaxSimDotProduct(scoreScript, invalidQueryVector, fieldName) ); assertThat( e.getMessage(), @@ -120,16 +120,16 @@ public void testByteMultiVectorClassBindings() throws IOException { List> invalidQueryVector = List.of(Arrays.asList((byte) 1, (byte) 1)); List hexidecimalString = List.of(HexFormat.of().formatHex(new byte[] { 1, 125, -12, 2, 4 })); - List fields = List.of( - new ByteMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.BYTE), - MultiDenseVectorScriptDocValuesTests.wrap(magnitudes), + List fields = List.of( + new ByteRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.BYTE), + RankVectorsScriptDocValuesTests.wrap(magnitudes), "test", ElementType.BYTE, dims ) ); - for (MultiDenseVectorDocValuesField field : fields) { + for (RankVectorsDocValuesField field : fields) { field.setNextDocId(0); ScoreScript scoreScript = mock(ScoreScript.class); @@ -174,16 +174,16 @@ public void testBitMultiVectorClassBindingsDotProduct() throws IOException { List> invalidQueryVector = List.of(Arrays.asList((byte) 1, (byte) 1)); List hexidecimalString = List.of(HexFormat.of().formatHex(new byte[] { 124 })); - List fields = List.of( - new BitMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.BIT), - MultiDenseVectorScriptDocValuesTests.wrap(new float[][] { { 5 } }), + List fields = List.of( + new BitRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.BIT), + RankVectorsScriptDocValuesTests.wrap(new float[][] { { 5 } }), "test", ElementType.BIT, dims ) ); - for (MultiDenseVectorDocValuesField field : fields) { + for (RankVectorsDocValuesField field : fields) { field.setNextDocId(0); ScoreScript scoreScript = mock(ScoreScript.class); @@ -240,23 +240,23 @@ public void testByteVsFloatSimilarity() throws IOException { float[][] floatVector = new float[][] { { 1f, 125f, -12f, 2f, 4f } }; byte[][] byteVector = new byte[][] { { (byte) 1, (byte) 125, (byte) -12, (byte) 2, (byte) 4 } }; - List fields = List.of( - new FloatMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.FLOAT), - MultiDenseVectorScriptDocValuesTests.wrap(magnitudes), + List fields = List.of( + new FloatRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.FLOAT), + RankVectorsScriptDocValuesTests.wrap(magnitudes), "field1", ElementType.FLOAT, dims ), - new ByteMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.BYTE), - MultiDenseVectorScriptDocValuesTests.wrap(magnitudes), + new ByteRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(new float[][][] { docVector }, ElementType.BYTE), + RankVectorsScriptDocValuesTests.wrap(magnitudes), "field3", ElementType.BYTE, dims ) ); - for (MultiDenseVectorDocValuesField field : fields) { + for (RankVectorsDocValuesField field : fields) { field.setNextDocId(0); ScoreScript scoreScript = mock(ScoreScript.class); @@ -296,17 +296,17 @@ public void testByteBoundaries() throws IOException { List> lessThanVector = List.of(List.of(-129)); List> decimalVector = List.of(List.of(0.5)); - List fields = List.of( - new ByteMultiDenseVectorDocValuesField( - MultiDenseVectorScriptDocValuesTests.wrap(new float[][][] { { docVector } }, ElementType.BYTE), - MultiDenseVectorScriptDocValuesTests.wrap(new float[][] { { 1 } }), + List fields = List.of( + new ByteRankVectorsDocValuesField( + RankVectorsScriptDocValuesTests.wrap(new float[][][] { { docVector } }, ElementType.BYTE), + RankVectorsScriptDocValuesTests.wrap(new float[][] { { 1 } }), "test", ElementType.BYTE, dims ) ); - for (MultiDenseVectorDocValuesField field : fields) { + for (RankVectorsDocValuesField field : fields) { field.setNextDocId(0); ScoreScript scoreScript = mock(ScoreScript.class); diff --git a/server/src/test/java/org/elasticsearch/script/field/vectors/MultiDenseVectorTests.java b/server/src/test/java/org/elasticsearch/script/field/vectors/RankVectorsTests.java similarity index 80% rename from server/src/test/java/org/elasticsearch/script/field/vectors/MultiDenseVectorTests.java rename to server/src/test/java/org/elasticsearch/script/field/vectors/RankVectorsTests.java index 12f4b931b4d0..ca7608b10aed 100644 --- a/server/src/test/java/org/elasticsearch/script/field/vectors/MultiDenseVectorTests.java +++ b/server/src/test/java/org/elasticsearch/script/field/vectors/RankVectorsTests.java @@ -11,7 +11,7 @@ import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.VectorUtil; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorFieldMapper; +import org.elasticsearch.index.mapper.vectors.RankVectorsFieldMapper; import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; @@ -19,11 +19,11 @@ import java.nio.ByteOrder; import java.util.function.IntFunction; -public class MultiDenseVectorTests extends ESTestCase { +public class RankVectorsTests extends ESTestCase { @BeforeClass public static void setup() { - assumeTrue("Requires multi-dense vector support", MultiDenseVectorFieldMapper.FEATURE_FLAG.isEnabled()); + assumeTrue("Requires rank-vectors support", RankVectorsFieldMapper.FEATURE_FLAG.isEnabled()); } public void testByteUnsupported() { @@ -38,7 +38,7 @@ public void testByteUnsupported() { } } - MultiDenseVector knn = newByteVector(docVector); + RankVectors knn = newByteVector(docVector); UnsupportedOperationException e; e = expectThrows(UnsupportedOperationException.class, () -> knn.maxSimDotProduct(queryVector)); @@ -57,20 +57,20 @@ public void testFloatUnsupported() { } } - MultiDenseVector knn = newFloatVector(docVector); + RankVectors knn = newFloatVector(docVector); UnsupportedOperationException e = expectThrows(UnsupportedOperationException.class, () -> knn.maxSimDotProduct(queryVector)); assertEquals(e.getMessage(), "use [float maxSimDotProduct(float[][] queryVector)] instead"); } - static MultiDenseVector newFloatVector(float[][] vector) { + static RankVectors newFloatVector(float[][] vector) { BytesRef magnitudes = magnitudes(vector.length, i -> (float) Math.sqrt(VectorUtil.dotProduct(vector[i], vector[i]))); - return new FloatMultiDenseVector(VectorIterator.from(vector), magnitudes, vector.length, vector[0].length); + return new FloatRankVectors(VectorIterator.from(vector), magnitudes, vector.length, vector[0].length); } - static MultiDenseVector newByteVector(byte[][] vector) { + static RankVectors newByteVector(byte[][] vector) { BytesRef magnitudes = magnitudes(vector.length, i -> (float) Math.sqrt(VectorUtil.dotProduct(vector[i], vector[i]))); - return new ByteMultiDenseVector(VectorIterator.from(vector), magnitudes, vector.length, vector[0].length); + return new ByteRankVectors(VectorIterator.from(vector), magnitudes, vector.length, vector[0].length); } static BytesRef magnitudes(int count, IntFunction magnitude) { diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index 51f66418bb44..d491e4bb52fa 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -112,7 +112,7 @@ import org.elasticsearch.index.mapper.TextFieldMapper; import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; -import org.elasticsearch.index.mapper.vectors.MultiDenseVectorFieldMapper; +import org.elasticsearch.index.mapper.vectors.RankVectorsFieldMapper; import org.elasticsearch.index.mapper.vectors.SparseVectorFieldMapper; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.shard.IndexShard; @@ -206,7 +206,7 @@ public abstract class AggregatorTestCase extends ESTestCase { private static final List TYPE_TEST_BLACKLIST = List.of( ObjectMapper.CONTENT_TYPE, // Cannot aggregate objects DenseVectorFieldMapper.CONTENT_TYPE, // Cannot aggregate dense vectors - MultiDenseVectorFieldMapper.CONTENT_TYPE, // Cannot aggregate dense vectors + RankVectorsFieldMapper.CONTENT_TYPE, // Cannot aggregate dense vectors SparseVectorFieldMapper.CONTENT_TYPE, // Sparse vectors are no longer supported NestedObjectMapper.CONTENT_TYPE, // TODO support for nested From 98e69c890cfa9b61c201d0a77dc5e3e9560b3fb5 Mon Sep 17 00:00:00 2001 From: Dan Rubinstein Date: Mon, 9 Dec 2024 13:43:06 -0500 Subject: [PATCH 27/39] Adding deprecation warning for data_frame_transforms roles (#117521) * Adding deprecation warning for data_frame_transforms roles * Updating deprecation warning URL --------- Co-authored-by: Elastic Machine --- .../core/transform/TransformDeprecations.java | 7 ++ .../transform/transforms/TransformConfig.java | 39 +++++++++- .../transforms/TransformConfigTests.java | 77 ++++++++++++++++++- 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformDeprecations.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformDeprecations.java index 79a679441de3..1de584d5593f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformDeprecations.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformDeprecations.java @@ -27,5 +27,12 @@ public class TransformDeprecations { public static final String MAX_PAGE_SEARCH_SIZE_BREAKING_CHANGES_URL = "https://ela.st/es-deprecation-7-transform-max-page-search-size"; + public static final String DATA_FRAME_TRANSFORMS_ROLES_BREAKING_CHANGES_URL = + "https://ela.st/es-deprecation-9-data-frame-transforms-roles"; + + public static final String DATA_FRAME_TRANSFORMS_ROLES_IS_DEPRECATED = "This transform configuration uses one or more obsolete roles " + + "prefixed with [data_frame_transformers_] which will be unsupported after the next upgrade. Switch to a user with the equivalent " + + "roles prefixed with [transform_] and use [/_transform/_upgrade] to upgrade all transforms to the latest roles.";; + private TransformDeprecations() {} } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfig.java index d8972dcf6a6b..745da7153999 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfig.java @@ -24,11 +24,13 @@ import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.common.time.TimeUtils; import org.elasticsearch.xpack.core.common.validation.SourceDestValidator; import org.elasticsearch.xpack.core.common.validation.SourceDestValidator.SourceDestValidation; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue.Level; +import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; import org.elasticsearch.xpack.core.transform.TransformConfigVersion; import org.elasticsearch.xpack.core.transform.TransformDeprecations; @@ -41,6 +43,7 @@ import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -49,6 +52,7 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.AUTHENTICATION_KEY; /** * This class holds the configuration details of a data frame transform @@ -65,6 +69,10 @@ public final class TransformConfig implements SimpleDiffable, W public static final ParseField HEADERS = new ParseField("headers"); /** Version in which {@code FieldCapabilitiesRequest.runtime_fields} field was introduced. */ private static final TransportVersion FIELD_CAPS_RUNTIME_MAPPINGS_INTRODUCED_TRANSPORT_VERSION = TransportVersions.V_7_12_0; + private static final List DEPRECATED_DATA_FRAME_TRANSFORMS_ROLES = List.of( + "data_frame_transforms_admin", + "data_frame_transforms_user" + ); /** Specifies all the possible transform functions. */ public enum Function { @@ -374,7 +382,7 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio * @param namedXContentRegistry XContent registry required for aggregations and query DSL * @return The deprecations of this transform */ - public List checkForDeprecations(NamedXContentRegistry namedXContentRegistry) { + public List checkForDeprecations(NamedXContentRegistry namedXContentRegistry) throws IOException { List deprecations = new ArrayList<>(); @@ -404,9 +412,38 @@ public List checkForDeprecations(NamedXContentRegistry namedXC if (retentionPolicyConfig != null) { retentionPolicyConfig.checkForDeprecations(getId(), namedXContentRegistry, deprecations::add); } + + var deprecatedTransformRoles = getRolesFromHeaders().stream().filter(DEPRECATED_DATA_FRAME_TRANSFORMS_ROLES::contains).toList(); + if (deprecatedTransformRoles.isEmpty() == false) { + deprecations.add( + new DeprecationIssue( + Level.CRITICAL, + "Transform [" + id + "] uses deprecated transform roles " + deprecatedTransformRoles, + TransformDeprecations.DATA_FRAME_TRANSFORMS_ROLES_BREAKING_CHANGES_URL, + TransformDeprecations.DATA_FRAME_TRANSFORMS_ROLES_IS_DEPRECATED, + false, + null + ) + ); + } + return deprecations; } + private List getRolesFromHeaders() throws IOException { + if (headers == null) { + return Collections.emptyList(); + } + + var encodedAuthenticationHeader = ClientHelper.filterSecurityHeaders(headers).getOrDefault(AUTHENTICATION_KEY, ""); + if (encodedAuthenticationHeader.isEmpty()) { + return Collections.emptyList(); + } + + var decodedAuthenticationHeader = AuthenticationContextSerializer.decode(encodedAuthenticationHeader); + return Arrays.asList(decodedAuthenticationHeader.getEffectiveSubject().getUser().roles()); + } + @Override public void writeTo(final StreamOutput out) throws IOException { out.writeString(id); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfigTests.java index 8cfecc432c66..2e7e5293c835 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformConfigTests.java @@ -27,6 +27,8 @@ import org.elasticsearch.xpack.core.common.validation.SourceDestValidator.SourceDestValidation; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue.Level; +import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.transform.AbstractSerializingTransformTestCase; import org.elasticsearch.xpack.core.transform.TransformConfigVersion; import org.elasticsearch.xpack.core.transform.TransformDeprecations; @@ -44,6 +46,7 @@ import java.util.Map; import static org.elasticsearch.test.TestMatchers.matchesPattern; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.AUTHENTICATION_KEY; import static org.elasticsearch.xpack.core.transform.transforms.DestConfigTests.randomDestConfig; import static org.elasticsearch.xpack.core.transform.transforms.SourceConfigTests.randomInvalidSourceConfig; import static org.elasticsearch.xpack.core.transform.transforms.SourceConfigTests.randomSourceConfig; @@ -58,6 +61,8 @@ public class TransformConfigTests extends AbstractSerializingTransformTestCase headers) { + return new TransformConfig( + randomAlphaOfLengthBetween(1, 10), + randomSourceConfig(), + randomDestConfig(), + randomBoolean() ? null : TimeValue.timeValueMillis(randomIntBetween(1_000, 3_600_000)), + randomBoolean() ? null : randomSyncConfig(), + headers, + randomBoolean() ? null : PivotConfigTests.randomPivotConfig(), + randomBoolean() ? null : LatestConfigTests.randomLatestConfig(), + randomBoolean() ? null : randomAlphaOfLengthBetween(1, 1000), + randomBoolean() ? null : SettingsConfigTests.randomSettingsConfig(), + randomBoolean() ? null : randomMetadata(), + randomBoolean() ? null : randomRetentionPolicyConfig(), + randomBoolean() ? null : Instant.now(), + TransformConfigVersion.CURRENT.toString() + ); + } + public static TransformConfig randomTransformConfig( String id, TransformConfigVersion version, @@ -915,10 +939,13 @@ public void testGroupByStayInOrder() throws IOException { } } - public void testCheckForDeprecations() { + public void testCheckForDeprecations_NoDeprecationWarnings() throws IOException { String id = randomAlphaOfLengthBetween(1, 10); assertThat(randomTransformConfig(id, TransformConfigVersion.CURRENT).checkForDeprecations(xContentRegistry()), is(empty())); + } + public void testCheckForDeprecations_WithDeprecatedFields_VersionCurrent() throws IOException { + String id = randomAlphaOfLengthBetween(1, 10); TransformConfig deprecatedConfig = randomTransformConfigWithDeprecatedFields(id, TransformConfigVersion.CURRENT); // check _and_ clear warnings @@ -940,8 +967,11 @@ public void testCheckForDeprecations() { ) ) ); + } - deprecatedConfig = randomTransformConfigWithDeprecatedFields(id, TransformConfigVersion.V_7_10_0); + public void testCheckForDeprecations_WithDeprecatedFields_Version_7_10() throws IOException { + String id = randomAlphaOfLengthBetween(1, 10); + TransformConfig deprecatedConfig = randomTransformConfigWithDeprecatedFields(id, TransformConfigVersion.V_7_10_0); // check _and_ clear warnings assertWarnings(TransformDeprecations.ACTION_MAX_PAGE_SEARCH_SIZE_IS_DEPRECATED); @@ -962,8 +992,11 @@ public void testCheckForDeprecations() { ) ) ); + } - deprecatedConfig = randomTransformConfigWithDeprecatedFields(id, TransformConfigVersion.V_7_4_0); + public void testCheckForDeprecations_WithDeprecatedFields_Version_7_4() throws IOException { + String id = randomAlphaOfLengthBetween(1, 10); + TransformConfig deprecatedConfig = randomTransformConfigWithDeprecatedFields(id, TransformConfigVersion.V_7_4_0); // check _and_ clear warnings assertWarnings(TransformDeprecations.ACTION_MAX_PAGE_SEARCH_SIZE_IS_DEPRECATED); @@ -994,6 +1027,44 @@ public void testCheckForDeprecations() { ); } + public void testCheckForDeprecations_WithDeprecatedTransformUserAdmin() throws IOException { + testCheckForDeprecations_WithDeprecatedRoles(List.of(DATA_FRAME_TRANSFORMS_ADMIN_ROLE)); + } + + public void testCheckForDeprecations_WithDeprecatedTransformUserRole() throws IOException { + testCheckForDeprecations_WithDeprecatedRoles(List.of(DATA_FRAME_TRANSFORMS_USER_ROLE)); + } + + public void testCheckForDeprecations_WithDeprecatedTransformRoles() throws IOException { + testCheckForDeprecations_WithDeprecatedRoles(List.of(DATA_FRAME_TRANSFORMS_ADMIN_ROLE, DATA_FRAME_TRANSFORMS_USER_ROLE)); + } + + private void testCheckForDeprecations_WithDeprecatedRoles(List roles) throws IOException { + var authentication = AuthenticationTestHelper.builder() + .realm() + .user(new User(randomAlphaOfLength(10), roles.toArray(String[]::new))) + .build(); + Map headers = Map.of(AUTHENTICATION_KEY, authentication.encode()); + TransformConfig deprecatedConfig = randomTransformConfigWithHeaders(headers); + + // important: checkForDeprecations does _not_ create new deprecation warnings + assertThat( + deprecatedConfig.checkForDeprecations(xContentRegistry()), + equalTo( + List.of( + new DeprecationIssue( + Level.CRITICAL, + "Transform [" + deprecatedConfig.getId() + "] uses deprecated transform roles " + roles, + TransformDeprecations.DATA_FRAME_TRANSFORMS_ROLES_BREAKING_CHANGES_URL, + TransformDeprecations.DATA_FRAME_TRANSFORMS_ROLES_IS_DEPRECATED, + false, + null + ) + ) + ) + ); + } + public void testSerializingMetadataPreservesOrder() throws IOException { String json = Strings.format(""" { From eb59b989efcb47b181cd4acf83689f97368152a4 Mon Sep 17 00:00:00 2001 From: Carlos Delgado <6339205+carlosdelest@users.noreply.github.com> Date: Mon, 9 Dec 2024 19:56:10 +0100 Subject: [PATCH 28/39] ESQL: Expand type compatibility for match function and operator (#117555) --- docs/changelog/117555.yaml | 5 + .../esql/functions/description/match.asciidoc | 2 +- .../esql/functions/description/qstr.asciidoc | 2 +- .../functions/kibana/definition/match.json | 466 +++++- .../kibana/definition/match_operator.json | 466 +++++- .../functions/kibana/definition/qstr.json | 2 +- .../esql/functions/kibana/docs/match.md | 2 +- .../functions/kibana/docs/match_operator.md | 2 +- .../esql/functions/kibana/docs/qstr.md | 2 +- .../esql/functions/parameters/match.asciidoc | 2 +- .../esql/functions/search-functions.asciidoc | 8 + docs/reference/esql/functions/search.asciidoc | 5 +- .../esql/functions/types/match.asciidoc | 29 +- .../functions/types/match_operator.asciidoc | 29 +- x-pack/plugin/build.gradle | 3 +- .../xpack/esql/CsvTestsDataLoader.java | 6 + .../xpack/esql/EsqlTestUtils.java | 4 +- .../elasticsearch/xpack/esql/SpecReader.java | 2 +- .../main/resources/employees_incompatible.csv | 101 ++ .../src/main/resources/mapping-all-types.json | 3 + .../src/main/resources/mapping-basic.json | 3 + .../mapping-default-incompatible.json | 80 + .../main/resources/match-function.csv-spec | 326 ++++ .../main/resources/match-operator.csv-spec | 316 ++++ .../xpack/esql/plugin/MatchFunctionIT.java | 10 - .../xpack/esql/plugin/MatchOperatorIT.java | 13 +- .../esql/src/main/antlr/EsqlBaseParser.g4 | 2 +- .../xpack/esql/action/EsqlCapabilities.java | 7 +- .../function/fulltext/FullTextFunction.java | 24 +- .../expression/function/fulltext/Match.java | 134 +- .../function/fulltext/QueryString.java | 3 +- .../comparison/EsqlBinaryComparison.java | 26 +- .../physical/local/PushFiltersToSource.java | 3 - .../xpack/esql/parser/EsqlBaseParser.interp | 2 +- .../xpack/esql/parser/EsqlBaseParser.java | 1408 +++++++++-------- .../xpack/esql/parser/ExpressionBuilder.java | 19 +- .../planner/EsqlExpressionTranslators.java | 25 +- .../xpack/esql/analysis/AnalyzerTests.java | 218 ++- .../xpack/esql/analysis/VerifierTests.java | 5 - .../function/AbstractFunctionTestCase.java | 5 +- .../function/fulltext/MatchOperatorTests.java | 13 +- .../function/fulltext/MatchTests.java | 426 ++++- .../LocalLogicalPlanOptimizerTests.java | 1 + .../LocalPhysicalPlanOptimizerTests.java | 228 ++- .../optimizer/PhysicalPlanOptimizerTests.java | 16 +- .../esql/parser/StatementParserTests.java | 24 +- .../test/esql/180_match_operator.yml | 34 +- 47 files changed, 3557 insertions(+), 955 deletions(-) create mode 100644 docs/changelog/117555.yaml create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees_incompatible.csv create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-default-incompatible.json diff --git a/docs/changelog/117555.yaml b/docs/changelog/117555.yaml new file mode 100644 index 000000000000..7891ab6d93a6 --- /dev/null +++ b/docs/changelog/117555.yaml @@ -0,0 +1,5 @@ +pr: 117555 +summary: Expand type compatibility for match function and operator +area: ES|QL +type: feature +issues: [] diff --git a/docs/reference/esql/functions/description/match.asciidoc b/docs/reference/esql/functions/description/match.asciidoc index 2a27fe481439..25f0571878d4 100644 --- a/docs/reference/esql/functions/description/match.asciidoc +++ b/docs/reference/esql/functions/description/match.asciidoc @@ -2,4 +2,4 @@ *Description* -Performs a match query on the specified field. Returns true if the provided query matches the row. +Performs a <> on the specified field. Returns true if the provided query matches the row. diff --git a/docs/reference/esql/functions/description/qstr.asciidoc b/docs/reference/esql/functions/description/qstr.asciidoc index 5ce9316405ad..d9dbe364f607 100644 --- a/docs/reference/esql/functions/description/qstr.asciidoc +++ b/docs/reference/esql/functions/description/qstr.asciidoc @@ -2,4 +2,4 @@ *Description* -Performs a query string query. Returns true if the provided query string matches the row. +Performs a <>. Returns true if the provided query string matches the row. diff --git a/docs/reference/esql/functions/kibana/definition/match.json b/docs/reference/esql/functions/kibana/definition/match.json index 4a5b05a3f501..7f2a8239cc0d 100644 --- a/docs/reference/esql/functions/kibana/definition/match.json +++ b/docs/reference/esql/functions/kibana/definition/match.json @@ -2,21 +2,75 @@ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", "type" : "eval", "name" : "match", - "description" : "Performs a match query on the specified field. Returns true if the provided query matches the row.", + "description" : "Performs a <> on the specified field. Returns true if the provided query matches the row.", "signatures" : [ { "params" : [ { "name" : "field", + "type" : "boolean", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "boolean", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "boolean", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", "type" : "keyword", "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "date", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date", + "optional" : false, "description" : "Field that the query will target." }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Value to find in the provided field." } ], "variadic" : false, @@ -26,15 +80,51 @@ "params" : [ { "name" : "field", + "type" : "date_nanos", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "date_nanos", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date_nanos", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", "type" : "keyword", "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "double", + "optional" : false, "description" : "Field that the query will target." }, { "name" : "query", - "type" : "text", + "type" : "double", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Value to find in the provided field." } ], "variadic" : false, @@ -44,7 +134,25 @@ "params" : [ { "name" : "field", - "type" : "text", + "type" : "double", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "double", "optional" : false, "description" : "Field that the query will target." }, @@ -52,7 +160,7 @@ "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Value to find in the provided field." } ], "variadic" : false, @@ -62,15 +170,357 @@ "params" : [ { "name" : "field", - "type" : "text", + "type" : "double", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "double", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "ip", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "ip", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "ip", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "keyword", "optional" : false, "description" : "Field that the query will target." }, { "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "double", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", "type" : "text", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "double", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "unsigned_long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "version", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "version", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "version", + "optional" : false, + "description" : "Value to find in the provided field." } ], "variadic" : false, diff --git a/docs/reference/esql/functions/kibana/definition/match_operator.json b/docs/reference/esql/functions/kibana/definition/match_operator.json index 7a0ace6168b5..44233bbddb65 100644 --- a/docs/reference/esql/functions/kibana/definition/match_operator.json +++ b/docs/reference/esql/functions/kibana/definition/match_operator.json @@ -2,21 +2,75 @@ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", "type" : "operator", "name" : "match_operator", - "description" : "Performs a match query on the specified field. Returns true if the provided query matches the row.", + "description" : "Performs a <> on the specified field. Returns true if the provided query matches the row.", "signatures" : [ { "params" : [ { "name" : "field", + "type" : "boolean", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "boolean", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "boolean", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", "type" : "keyword", "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "date", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date", + "optional" : false, "description" : "Field that the query will target." }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Value to find in the provided field." } ], "variadic" : false, @@ -26,15 +80,51 @@ "params" : [ { "name" : "field", + "type" : "date_nanos", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "date_nanos", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "date_nanos", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", "type" : "keyword", "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "double", + "optional" : false, "description" : "Field that the query will target." }, { "name" : "query", - "type" : "text", + "type" : "double", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Value to find in the provided field." } ], "variadic" : false, @@ -44,7 +134,25 @@ "params" : [ { "name" : "field", - "type" : "text", + "type" : "double", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "double", "optional" : false, "description" : "Field that the query will target." }, @@ -52,7 +160,7 @@ "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Value to find in the provided field." } ], "variadic" : false, @@ -62,15 +170,357 @@ "params" : [ { "name" : "field", - "type" : "text", + "type" : "double", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "double", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "ip", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "ip", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "ip", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "keyword", "optional" : false, "description" : "Field that the query will target." }, { "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "double", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", "type" : "text", "optional" : false, - "description" : "Text you wish to find in the provided field." + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "double", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "integer", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "unsigned_long", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "unsigned_long", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "version", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "keyword", + "optional" : false, + "description" : "Value to find in the provided field." + } + ], + "variadic" : false, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "version", + "optional" : false, + "description" : "Field that the query will target." + }, + { + "name" : "query", + "type" : "version", + "optional" : false, + "description" : "Value to find in the provided field." } ], "variadic" : false, diff --git a/docs/reference/esql/functions/kibana/definition/qstr.json b/docs/reference/esql/functions/kibana/definition/qstr.json index 76473349a341..3b091bfe2e13 100644 --- a/docs/reference/esql/functions/kibana/definition/qstr.json +++ b/docs/reference/esql/functions/kibana/definition/qstr.json @@ -2,7 +2,7 @@ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", "type" : "eval", "name" : "qstr", - "description" : "Performs a query string query. Returns true if the provided query string matches the row.", + "description" : "Performs a <>. Returns true if the provided query string matches the row.", "signatures" : [ { "params" : [ diff --git a/docs/reference/esql/functions/kibana/docs/match.md b/docs/reference/esql/functions/kibana/docs/match.md index b866637b41b8..adf6de91c90f 100644 --- a/docs/reference/esql/functions/kibana/docs/match.md +++ b/docs/reference/esql/functions/kibana/docs/match.md @@ -3,7 +3,7 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ --> ### MATCH -Performs a match query on the specified field. Returns true if the provided query matches the row. +Performs a <> on the specified field. Returns true if the provided query matches the row. ``` FROM books diff --git a/docs/reference/esql/functions/kibana/docs/match_operator.md b/docs/reference/esql/functions/kibana/docs/match_operator.md index fda8b24ff76c..b0b619679808 100644 --- a/docs/reference/esql/functions/kibana/docs/match_operator.md +++ b/docs/reference/esql/functions/kibana/docs/match_operator.md @@ -3,7 +3,7 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ --> ### MATCH_OPERATOR -Performs a match query on the specified field. Returns true if the provided query matches the row. +Performs a <> on the specified field. Returns true if the provided query matches the row. ``` FROM books diff --git a/docs/reference/esql/functions/kibana/docs/qstr.md b/docs/reference/esql/functions/kibana/docs/qstr.md index 9b5dc3f9a22e..7df5a2fe08a9 100644 --- a/docs/reference/esql/functions/kibana/docs/qstr.md +++ b/docs/reference/esql/functions/kibana/docs/qstr.md @@ -3,7 +3,7 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ --> ### QSTR -Performs a query string query. Returns true if the provided query string matches the row. +Performs a <>. Returns true if the provided query string matches the row. ``` FROM books diff --git a/docs/reference/esql/functions/parameters/match.asciidoc b/docs/reference/esql/functions/parameters/match.asciidoc index f18adb28cd20..46f6acad9e12 100644 --- a/docs/reference/esql/functions/parameters/match.asciidoc +++ b/docs/reference/esql/functions/parameters/match.asciidoc @@ -6,4 +6,4 @@ Field that the query will target. `query`:: -Text you wish to find in the provided field. +Value to find in the provided field. diff --git a/docs/reference/esql/functions/search-functions.asciidoc b/docs/reference/esql/functions/search-functions.asciidoc index 943a262497d4..238813c382c8 100644 --- a/docs/reference/esql/functions/search-functions.asciidoc +++ b/docs/reference/esql/functions/search-functions.asciidoc @@ -5,6 +5,14 @@ Full-text Search functions ++++ +Full text functions are used to search for text in fields. +<> is used to analyze the query before it is searched. + +Full text functions can be used to match <>. +A multivalued field that contains a value that matches a full text query is considered to match the query. + +See <> for information on the limitations of full text search. + {esql} supports these full-text search functions: // tag::search_list[] diff --git a/docs/reference/esql/functions/search.asciidoc b/docs/reference/esql/functions/search.asciidoc index ae1b003b65ab..ba399ead8adf 100644 --- a/docs/reference/esql/functions/search.asciidoc +++ b/docs/reference/esql/functions/search.asciidoc @@ -6,7 +6,10 @@ The only search operator is match (`:`). preview::["Do not use on production environments. This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features."] -The match operator performs a <> on the specified field. Returns true if the provided query matches the row. +The match operator performs a <> on the specified field. +Returns true if the provided query matches the row. + +The match operator is equivalent to the <>. [.text-center] image::esql/functions/signature/match_operator.svg[Embedded,opts=inline] diff --git a/docs/reference/esql/functions/types/match.asciidoc b/docs/reference/esql/functions/types/match.asciidoc index 7523b29c62b1..402277af4474 100644 --- a/docs/reference/esql/functions/types/match.asciidoc +++ b/docs/reference/esql/functions/types/match.asciidoc @@ -5,8 +5,33 @@ [%header.monospaced.styled,format=dsv,separator=|] |=== field | query | result +boolean | boolean | boolean +boolean | keyword | boolean +date | date | boolean +date | keyword | boolean +date_nanos | date_nanos | boolean +date_nanos | keyword | boolean +double | double | boolean +double | integer | boolean +double | keyword | boolean +double | long | boolean +integer | double | boolean +integer | integer | boolean +integer | keyword | boolean +integer | long | boolean +ip | ip | boolean +ip | keyword | boolean keyword | keyword | boolean -keyword | text | boolean +long | double | boolean +long | integer | boolean +long | keyword | boolean +long | long | boolean text | keyword | boolean -text | text | boolean +unsigned_long | double | boolean +unsigned_long | integer | boolean +unsigned_long | keyword | boolean +unsigned_long | long | boolean +unsigned_long | unsigned_long | boolean +version | keyword | boolean +version | version | boolean |=== diff --git a/docs/reference/esql/functions/types/match_operator.asciidoc b/docs/reference/esql/functions/types/match_operator.asciidoc index 7523b29c62b1..402277af4474 100644 --- a/docs/reference/esql/functions/types/match_operator.asciidoc +++ b/docs/reference/esql/functions/types/match_operator.asciidoc @@ -5,8 +5,33 @@ [%header.monospaced.styled,format=dsv,separator=|] |=== field | query | result +boolean | boolean | boolean +boolean | keyword | boolean +date | date | boolean +date | keyword | boolean +date_nanos | date_nanos | boolean +date_nanos | keyword | boolean +double | double | boolean +double | integer | boolean +double | keyword | boolean +double | long | boolean +integer | double | boolean +integer | integer | boolean +integer | keyword | boolean +integer | long | boolean +ip | ip | boolean +ip | keyword | boolean keyword | keyword | boolean -keyword | text | boolean +long | double | boolean +long | integer | boolean +long | keyword | boolean +long | long | boolean text | keyword | boolean -text | text | boolean +unsigned_long | double | boolean +unsigned_long | integer | boolean +unsigned_long | keyword | boolean +unsigned_long | long | boolean +unsigned_long | unsigned_long | boolean +version | keyword | boolean +version | version | boolean |=== diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle index 26040529b04d..fb37fb357555 100644 --- a/x-pack/plugin/build.gradle +++ b/x-pack/plugin/build.gradle @@ -5,8 +5,6 @@ * 2.0. */ -import org.elasticsearch.gradle.Version -import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask import org.elasticsearch.gradle.util.GradleUtils @@ -95,5 +93,6 @@ tasks.named("yamlRestCompatTestTransform").configure({ task -> task.skipTest("esql/80_text/values function", "The output type changed from TEXT to KEYWORD.") task.skipTest("privileges/11_builtin/Test get builtin privileges" ,"unnecessary to test compatibility") task.skipTest("esql/61_enrich_ip/Invalid IP strings", "We switched from exceptions to null+warnings for ENRICH runtime errors") + task.skipTest("esql/180_match_operator/match with non text field", "Match operator can now be used on non-text fields") }) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java index f9d8cf00695c..34af1edb9f99 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java @@ -52,6 +52,11 @@ public class CsvTestsDataLoader { private static final int BULK_DATA_SIZE = 100_000; private static final TestsDataset EMPLOYEES = new TestsDataset("employees", "mapping-default.json", "employees.csv").noSubfields(); + private static final TestsDataset EMPLOYEES_INCOMPATIBLE = new TestsDataset( + "employees_incompatible", + "mapping-default-incompatible.json", + "employees_incompatible.csv" + ).noSubfields(); private static final TestsDataset HOSTS = new TestsDataset("hosts"); private static final TestsDataset APPS = new TestsDataset("apps"); private static final TestsDataset APPS_SHORT = APPS.withIndex("apps_short").withTypeMapping(Map.of("id", "short")); @@ -103,6 +108,7 @@ public class CsvTestsDataLoader { public static final Map CSV_DATASET_MAP = Map.ofEntries( Map.entry(EMPLOYEES.indexName, EMPLOYEES), + Map.entry(EMPLOYEES_INCOMPATIBLE.indexName, EMPLOYEES_INCOMPATIBLE), Map.entry(HOSTS.indexName, HOSTS), Map.entry(APPS.indexName, APPS), Map.entry(APPS_SHORT.indexName, APPS_SHORT), diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java index 5535e801b1b0..18ce9d7e3e05 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java @@ -121,6 +121,7 @@ import static org.elasticsearch.test.ESTestCase.randomLong; import static org.elasticsearch.test.ESTestCase.randomLongBetween; import static org.elasticsearch.test.ESTestCase.randomMillisUpToYear9999; +import static org.elasticsearch.test.ESTestCase.randomNonNegativeLong; import static org.elasticsearch.test.ESTestCase.randomShort; import static org.elasticsearch.test.ESTestCase.randomZone; import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY; @@ -728,7 +729,8 @@ public static Literal randomLiteral(DataType type) { case BYTE -> randomByte(); case SHORT -> randomShort(); case INTEGER, COUNTER_INTEGER -> randomInt(); - case UNSIGNED_LONG, LONG, COUNTER_LONG -> randomLong(); + case LONG, COUNTER_LONG -> randomLong(); + case UNSIGNED_LONG -> randomNonNegativeLong(); case DATE_PERIOD -> Period.of(randomIntBetween(-1000, 1000), randomIntBetween(-13, 13), randomIntBetween(-32, 32)); case DATETIME -> randomMillisUpToYear9999(); case DATE_NANOS -> randomLongBetween(0, Long.MAX_VALUE); diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/SpecReader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/SpecReader.java index 793268f18184..760768cd0f11 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/SpecReader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/SpecReader.java @@ -86,7 +86,7 @@ public static List readURLSpec(URL source, Parser parser) throws Excep lineNumber++; } if (testName != null) { - throw new IllegalStateException("Read a test without a body at the end of [" + fileName + "]."); + throw new IllegalStateException("Read a test [" + testName + "] without a body at the end of [" + fileName + "]."); } } assertNull("Cannot find spec for test " + testName, testName); diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees_incompatible.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees_incompatible.csv new file mode 100644 index 000000000000..ddbdb89476c4 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees_incompatible.csv @@ -0,0 +1,101 @@ +birth_date:date_nanos ,emp_no:long,first_name:text,gender:text,hire_date:date_nanos,languages:byte,languages.long:long,languages.short:short,languages.byte:byte,last_name:text,salary:long,height:float,height.double:double,height.scaled_float:scaled_float,height.half_float:half_float,still_hired:keyword,avg_worked_seconds:unsigned_long,job_positions:text,is_rehired:keyword,salary_change:float,salary_change.int:integer,salary_change.long:long,salary_change.keyword:keyword +1953-09-02T00:00:00Z,10001,Georgi ,M,1986-06-26T00:00:00Z,2,2,2,2,Facello ,57305,2.03,2.03,2.03,2.03,true ,268728049,[Senior Python Developer,Accountant],[false,true],[1.19],[1],[1],[1.19] +1964-06-02T00:00:00Z,10002,Bezalel ,F,1985-11-21T00:00:00Z,5,5,5,5,Simmel ,56371,2.08,2.08,2.08,2.08,true ,328922887,[Senior Team Lead],[false,false],[-7.23,11.17],[-7,11],[-7,11],[-7.23,11.17] +1959-12-03T00:00:00Z,10003,Parto ,M,1986-08-28T00:00:00Z,4,4,4,4,Bamford ,61805,1.83,1.83,1.83,1.83,false,200296405,[],[],[14.68,12.82],[14,12],[14,12],[14.68,12.82] +1954-05-01T00:00:00Z,10004,Chirstian ,M,1986-12-01T00:00:00Z,5,5,5,5,Koblick ,36174,1.78,1.78,1.78,1.78,true ,311267831,[Reporting Analyst,Tech Lead,Head Human Resources,Support Engineer],[true],[3.65,-0.35,1.13,13.48],[3,0,1,13],[3,0,1,13],[3.65,-0.35,1.13,13.48] +1955-01-21T00:00:00Z,10005,Kyoichi ,M,1989-09-12T00:00:00Z,1,1,1,1,Maliniak ,63528,2.05,2.05,2.05,2.05,true ,244294991,[],[false,false,false,true],[-2.14,13.07],[-2,13],[-2,13],[-2.14,13.07] +1953-04-20T00:00:00Z,10006,Anneke ,F,1989-06-02T00:00:00Z,3,3,3,3,Preusig ,60335,1.56,1.56,1.56,1.56,false,372957040,[Tech Lead,Principal Support Engineer,Senior Team Lead],[],[-3.90],[-3],[-3],[-3.90] +1957-05-23T00:00:00Z,10007,Tzvetan ,F,1989-02-10T00:00:00Z,4,4,4,4,Zielinski ,74572,1.70,1.70,1.70,1.70,true ,393084805,[],[true,false,true,false],[-7.06,1.99,0.57],[-7,1,0],[-7,1,0],[-7.06,1.99,0.57] +1958-02-19T00:00:00Z,10008,Saniya ,M,1994-09-15T00:00:00Z,2,2,2,2,Kalloufi ,43906,2.10,2.10,2.10,2.10,true ,283074758,[Senior Python Developer,Junior Developer,Purchase Manager,Internship],[true,false],[12.68,3.54,0.75,-2.92],[12,3,0,-2],[12,3,0,-2],[12.68,3.54,0.75,-2.92] +1952-04-19T00:00:00Z,10009,Sumant ,F,1985-02-18T00:00:00Z,1,1,1,1,Peac ,66174,1.85,1.85,1.85,1.85,false,236805489,[Senior Python Developer,Internship],[],[],[],[],[] +1963-06-01T00:00:00Z,10010,Duangkaew , ,1989-08-24T00:00:00Z,4,4,4,4,Piveteau ,45797,1.70,1.70,1.70,1.70,false,315236372,[Architect,Reporting Analyst,Tech Lead,Purchase Manager],[true,true,false,false],[5.05,-6.77,4.69,12.15],[5,-6,4,12],[5,-6,4,12],[5.05,-6.77,4.69,12.15] +1953-11-07T00:00:00Z,10011,Mary , ,1990-01-22T00:00:00Z,5,5,5,5,Sluis ,31120,1.50,1.50,1.50,1.50,true ,239615525,[Architect,Reporting Analyst,Tech Lead,Senior Team Lead],[true,true],[10.35,-7.82,8.73,3.48],[10,-7,8,3],[10,-7,8,3],[10.35,-7.82,8.73,3.48] +1960-10-04T00:00:00Z,10012,Patricio , ,1992-12-18T00:00:00Z,5,5,5,5,Bridgland ,48942,1.97,1.97,1.97,1.97,false,365510850,[Head Human Resources,Accountant],[false,true,true,false],[0.04],[0],[0],[0.04] +1963-06-07T00:00:00Z,10013,Eberhardt , ,1985-10-20T00:00:00Z,1,1,1,1,Terkki ,48735,1.94,1.94,1.94,1.94,true ,253864340,[Reporting Analyst],[true,true],[],[],[],[] +1956-02-12T00:00:00Z,10014,Berni , ,1987-03-11T00:00:00Z,5,5,5,5,Genin ,37137,1.99,1.99,1.99,1.99,false,225049139,[Reporting Analyst,Data Scientist,Head Human Resources],[],[-1.89,9.07],[-1,9],[-1,9],[-1.89,9.07] +1959-08-19T00:00:00Z,10015,Guoxiang , ,1987-07-02T00:00:00Z,5,5,5,5,Nooteboom ,25324,1.66,1.66,1.66,1.66,true ,390266432,[Principal Support Engineer,Junior Developer,Head Human Resources,Support Engineer],[true,false,false,false],[14.25,12.40],[14,12],[14,12],[14.25,12.40] +1961-05-02T00:00:00Z,10016,Kazuhito , ,1995-01-27T00:00:00Z,2,2,2,2,Cappelletti ,61358,1.54,1.54,1.54,1.54,false,253029411,[Reporting Analyst,Python Developer,Accountant,Purchase Manager],[false,false],[-5.18,7.69],[-5,7],[-5,7],[-5.18,7.69] +1958-07-06T00:00:00Z,10017,Cristinel , ,1993-08-03T00:00:00Z,2,2,2,2,Bouloucos ,58715,1.74,1.74,1.74,1.74,false,236703986,[Data Scientist,Head Human Resources,Purchase Manager],[true,false,true,true],[-6.33],[-6],[-6],[-6.33] +1954-06-19T00:00:00Z,10018,Kazuhide , ,1987-04-03T00:00:00Z,2,2,2,2,Peha ,56760,1.97,1.97,1.97,1.97,false,309604079,[Junior Developer],[false,false,true,true],[-1.64,11.51,-5.32],[-1,11,-5],[-1,11,-5],[-1.64,11.51,-5.32] +1953-01-23T00:00:00Z,10019,Lillian , ,1999-04-30T00:00:00Z,1,1,1,1,Haddadi ,73717,2.06,2.06,2.06,2.06,false,342855721,[Purchase Manager],[false,false],[-6.84,8.42,-7.26],[-6,8,-7],[-6,8,-7],[-6.84,8.42,-7.26] +1952-12-24T00:00:00Z,10020,Mayuko ,M,1991-01-26T00:00:00Z, , , , ,Warwick ,40031,1.41,1.41,1.41,1.41,false,373309605,[Tech Lead],[true,true,false],[-5.81],[-5],[-5],[-5.81] +1960-02-20T00:00:00Z,10021,Ramzi ,M,1988-02-10T00:00:00Z, , , , ,Erde ,60408,1.47,1.47,1.47,1.47,false,287654610,[Support Engineer],[true],[],[],[],[] +1952-07-08T00:00:00Z,10022,Shahaf ,M,1995-08-22T00:00:00Z, , , , ,Famili ,48233,1.82,1.82,1.82,1.82,false,233521306,[Reporting Analyst,Data Scientist,Python Developer,Internship],[true,false],[12.09,2.85],[12,2],[12,2],[12.09,2.85] +1953-09-29T00:00:00Z,10023,Bojan ,F,1989-12-17T00:00:00Z, , , , ,Montemayor ,47896,1.75,1.75,1.75,1.75,true ,330870342,[Accountant,Support Engineer,Purchase Manager],[true,true,false],[14.63,0.80],[14,0],[14,0],[14.63,0.80] +1958-09-05T00:00:00Z,10024,Suzette ,F,1997-05-19T00:00:00Z, , , , ,Pettey ,64675,2.08,2.08,2.08,2.08,true ,367717671,[Junior Developer],[true,true,true,true],[],[],[],[] +1958-10-31T00:00:00Z,10025,Prasadram ,M,1987-08-17T00:00:00Z, , , , ,Heyers ,47411,1.87,1.87,1.87,1.87,false,371270797,[Accountant],[true,false],[-4.33,-2.90,12.06,-3.46],[-4,-2,12,-3],[-4,-2,12,-3],[-4.33,-2.90,12.06,-3.46] +1953-04-03T00:00:00Z,10026,Yongqiao ,M,1995-03-20T00:00:00Z, , , , ,Berztiss ,28336,2.10,2.10,2.10,2.10,true ,359208133,[Reporting Analyst],[false,true],[-7.37,10.62,11.20],[-7,10,11],[-7,10,11],[-7.37,10.62,11.20] +1962-07-10T00:00:00Z,10027,Divier ,F,1989-07-07T00:00:00Z, , , , ,Reistad ,73851,1.53,1.53,1.53,1.53,false,374037782,[Senior Python Developer],[false],[],[],[],[] +1963-11-26T00:00:00Z,10028,Domenick ,M,1991-10-22T00:00:00Z, , , , ,Tempesti ,39356,2.07,2.07,2.07,2.07,true ,226435054,[Tech Lead,Python Developer,Accountant,Internship],[true,false,false,true],[],[],[],[] +1956-12-13T00:00:00Z,10029,Otmar ,M,1985-11-20T00:00:00Z, , , , ,Herbst ,74999,1.99,1.99,1.99,1.99,false,257694181,[Senior Python Developer,Data Scientist,Principal Support Engineer],[true],[-0.32,-1.90,-8.19],[0,-1,-8],[0,-1,-8],[-0.32,-1.90,-8.19] +1958-07-14T00:00:00Z,10030, ,M,1994-02-17T00:00:00Z,3,3,3,3,Demeyer ,67492,1.92,1.92,1.92,1.92,false,394597613,[Tech Lead,Data Scientist,Senior Team Lead],[true,false,false],[-0.40],[0],[0],[-0.40] +1959-01-27T00:00:00Z,10031, ,M,1991-09-01T00:00:00Z,4,4,4,4,Joslin ,37716,1.68,1.68,1.68,1.68,false,348545109,[Architect,Senior Python Developer,Purchase Manager,Senior Team Lead],[false],[],[],[],[] +1960-08-09T00:00:00Z,10032, ,F,1990-06-20T00:00:00Z,3,3,3,3,Reistad ,62233,2.10,2.10,2.10,2.10,false,277622619,[Architect,Senior Python Developer,Junior Developer,Purchase Manager],[false,false],[9.32,-4.92],[9,-4],[9,-4],[9.32,-4.92] +1956-11-14T00:00:00Z,10033, ,M,1987-03-18T00:00:00Z,1,1,1,1,Merlo ,70011,1.63,1.63,1.63,1.63,false,208374744,[],[true],[],[],[],[] +1962-12-29T00:00:00Z,10034, ,M,1988-09-21T00:00:00Z,1,1,1,1,Swan ,39878,1.46,1.46,1.46,1.46,false,214393176,[Business Analyst,Data Scientist,Python Developer,Accountant],[false],[-8.46],[-8],[-8],[-8.46] +1953-02-08T00:00:00Z,10035, ,M,1988-09-05T00:00:00Z,5,5,5,5,Chappelet ,25945,1.81,1.81,1.81,1.81,false,203838153,[Senior Python Developer,Data Scientist],[false],[-2.54,-6.58],[-2,-6],[-2,-6],[-2.54,-6.58] +1959-08-10T00:00:00Z,10036, ,M,1992-01-03T00:00:00Z,4,4,4,4,Portugali ,60781,1.61,1.61,1.61,1.61,false,305493131,[Senior Python Developer],[true,false,false],[],[],[],[] +1963-07-22T00:00:00Z,10037, ,M,1990-12-05T00:00:00Z,2,2,2,2,Makrucki ,37691,2.00,2.00,2.00,2.00,true ,359217000,[Senior Python Developer,Tech Lead,Accountant],[false],[-7.08],[-7],[-7],[-7.08] +1960-07-20T00:00:00Z,10038, ,M,1989-09-20T00:00:00Z,4,4,4,4,Lortz ,35222,1.53,1.53,1.53,1.53,true ,314036411,[Senior Python Developer,Python Developer,Support Engineer],[],[],[],[],[] +1959-10-01T00:00:00Z,10039, ,M,1988-01-19T00:00:00Z,2,2,2,2,Brender ,36051,1.55,1.55,1.55,1.55,false,243221262,[Business Analyst,Python Developer,Principal Support Engineer],[true,true],[-6.90],[-6],[-6],[-6.90] + ,10040,Weiyi ,F,1993-02-14T00:00:00Z,4,4,4,4,Meriste ,37112,1.90,1.90,1.90,1.90,false,244478622,[Principal Support Engineer],[true,false,true,true],[6.97,14.74,-8.94,1.92],[6,14,-8,1],[6,14,-8,1],[6.97,14.74,-8.94,1.92] + ,10041,Uri ,F,1989-11-12T00:00:00Z,1,1,1,1,Lenart ,56415,1.75,1.75,1.75,1.75,false,287789442,[Data Scientist,Head Human Resources,Internship,Senior Team Lead],[],[9.21,0.05,7.29,-2.94],[9,0,7,-2],[9,0,7,-2],[9.21,0.05,7.29,-2.94] + ,10042,Magy ,F,1993-03-21T00:00:00Z,3,3,3,3,Stamatiou ,30404,1.44,1.44,1.44,1.44,true ,246355863,[Architect,Business Analyst,Junior Developer,Internship],[],[-9.28,9.42],[-9,9],[-9,9],[-9.28,9.42] + ,10043,Yishay ,M,1990-10-20T00:00:00Z,1,1,1,1,Tzvieli ,34341,1.52,1.52,1.52,1.52,true ,287222180,[Data Scientist,Python Developer,Support Engineer],[false,true,true],[-5.17,4.62,7.42],[-5,4,7],[-5,4,7],[-5.17,4.62,7.42] + ,10044,Mingsen ,F,1994-05-21T00:00:00Z,1,1,1,1,Casley ,39728,2.06,2.06,2.06,2.06,false,387408356,[Tech Lead,Principal Support Engineer,Accountant,Support Engineer],[true,true],[8.09],[8],[8],[8.09] + ,10045,Moss ,M,1989-09-02T00:00:00Z,3,3,3,3,Shanbhogue ,74970,1.70,1.70,1.70,1.70,false,371418933,[Principal Support Engineer,Junior Developer,Accountant,Purchase Manager],[true,false],[],[],[],[] + ,10046,Lucien ,M,1992-06-20T00:00:00Z,4,4,4,4,Rosenbaum ,50064,1.52,1.52,1.52,1.52,true ,302353405,[Principal Support Engineer,Junior Developer,Head Human Resources,Internship],[true,true,false,true],[2.39],[2],[2],[2.39] + ,10047,Zvonko ,M,1989-03-31T00:00:00Z,4,4,4,4,Nyanchama ,42716,1.52,1.52,1.52,1.52,true ,306369346,[Architect,Data Scientist,Principal Support Engineer,Senior Team Lead],[true],[-6.36,12.12],[-6,12],[-6,12],[-6.36,12.12] + ,10048,Florian ,M,1985-02-24T00:00:00Z,3,3,3,3,Syrotiuk ,26436,2.00,2.00,2.00,2.00,false,248451647,[Internship],[true,true],[],[],[],[] + ,10049,Basil ,F,1992-05-04T00:00:00Z,5,5,5,5,Tramer ,37853,1.52,1.52,1.52,1.52,true ,320725709,[Senior Python Developer,Business Analyst],[],[-1.05],[-1],[-1],[-1.05] +1958-05-21T00:00:00Z,10050,Yinghua ,M,1990-12-25T00:00:00Z,2,2,2,2,Dredge ,43026,1.96,1.96,1.96,1.96,true ,242731798,[Reporting Analyst,Junior Developer,Accountant,Support Engineer],[true],[8.70,10.94],[8,10],[8,10],[8.70,10.94] +1953-07-28T00:00:00Z,10051,Hidefumi ,M,1992-10-15T00:00:00Z,3,3,3,3,Caine ,58121,1.89,1.89,1.89,1.89,true ,374753122,[Business Analyst,Accountant,Purchase Manager],[],[],[],[],[] +1961-02-26T00:00:00Z,10052,Heping ,M,1988-05-21T00:00:00Z,1,1,1,1,Nitsch ,55360,1.79,1.79,1.79,1.79,true ,299654717,[],[true,true,false],[-0.55,-1.89,-4.22,-6.03],[0,-1,-4,-6],[0,-1,-4,-6],[-0.55,-1.89,-4.22,-6.03] +1954-09-13T00:00:00Z,10053,Sanjiv ,F,1986-02-04T00:00:00Z,3,3,3,3,Zschoche ,54462,1.58,1.58,1.58,1.58,false,368103911,[Support Engineer],[true,false,true,false],[-7.67,-3.25],[-7,-3],[-7,-3],[-7.67,-3.25] +1957-04-04T00:00:00Z,10054,Mayumi ,M,1995-03-13T00:00:00Z,4,4,4,4,Schueller ,65367,1.82,1.82,1.82,1.82,false,297441693,[Principal Support Engineer],[false,false],[],[],[],[] +1956-06-06T00:00:00Z,10055,Georgy ,M,1992-04-27T00:00:00Z,5,5,5,5,Dredge ,49281,2.04,2.04,2.04,2.04,false,283157844,[Senior Python Developer,Head Human Resources,Internship,Support Engineer],[false,false,true],[7.34,12.99,3.17],[7,12,3],[7,12,3],[7.34,12.99,3.17] +1961-09-01T00:00:00Z,10056,Brendon ,F,1990-02-01T00:00:00Z,2,2,2,2,Bernini ,33370,1.57,1.57,1.57,1.57,true ,349086555,[Senior Team Lead],[true,false,false],[10.99,-5.17],[10,-5],[10,-5],[10.99,-5.17] +1954-05-30T00:00:00Z,10057,Ebbe ,F,1992-01-15T00:00:00Z,4,4,4,4,Callaway ,27215,1.59,1.59,1.59,1.59,true ,324356269,[Python Developer,Head Human Resources],[],[-6.73,-2.43,-5.27,1.03],[-6,-2,-5,1],[-6,-2,-5,1],[-6.73,-2.43,-5.27,1.03] +1954-10-01T00:00:00Z,10058,Berhard ,M,1987-04-13T00:00:00Z,3,3,3,3,McFarlin ,38376,1.83,1.83,1.83,1.83,false,268378108,[Principal Support Engineer],[],[-4.89],[-4],[-4],[-4.89] +1953-09-19T00:00:00Z,10059,Alejandro ,F,1991-06-26T00:00:00Z,2,2,2,2,McAlpine ,44307,1.48,1.48,1.48,1.48,false,237368465,[Architect,Principal Support Engineer,Purchase Manager,Senior Team Lead],[false],[5.53,13.38,-4.69,6.27],[5,13,-4,6],[5,13,-4,6],[5.53,13.38,-4.69,6.27] +1961-10-15T00:00:00Z,10060,Breannda ,M,1987-11-02T00:00:00Z,2,2,2,2,Billingsley ,29175,1.42,1.42,1.42,1.42,true ,341158890,[Business Analyst,Data Scientist,Senior Team Lead],[false,false,true,false],[-1.76,-0.85],[-1,0],[-1,0],[-1.76,-0.85] +1962-10-19T00:00:00Z,10061,Tse ,M,1985-09-17T00:00:00Z,1,1,1,1,Herber ,49095,1.45,1.45,1.45,1.45,false,327550310,[Purchase Manager,Senior Team Lead],[false,true],[14.39,-2.58,-0.95],[14,-2,0],[14,-2,0],[14.39,-2.58,-0.95] +1961-11-02T00:00:00Z,10062,Anoosh ,M,1991-08-30T00:00:00Z,3,3,3,3,Peyn ,65030,1.70,1.70,1.70,1.70,false,203989706,[Python Developer,Senior Team Lead],[false,true,true],[-1.17],[-1],[-1],[-1.171] +1952-08-06T00:00:00Z,10063,Gino ,F,1989-04-08T00:00:00Z,3,3,3,3,Leonhardt ,52121,1.78,1.78,1.78,1.78,true ,214068302,[],[true],[],[],[],[] +1959-04-07T00:00:00Z,10064,Udi ,M,1985-11-20T00:00:00Z,5,5,5,5,Jansch ,33956,1.93,1.93,1.93,1.93,false,307364077,[Purchase Manager],[false,false,true,false],[-8.66,-2.52],[-8,-2],[-8,-2],[-8.66,-2.52] +1963-04-14T00:00:00Z,10065,Satosi ,M,1988-05-18T00:00:00Z,2,2,2,2,Awdeh ,50249,1.59,1.59,1.59,1.59,false,372660279,[Business Analyst,Data Scientist,Principal Support Engineer],[false,true],[-1.47,14.44,-9.81],[-1,14,-9],[-1,14,-9],[-1.47,14.44,-9.81] +1952-11-13T00:00:00Z,10066,Kwee ,M,1986-02-26T00:00:00Z,5,5,5,5,Schusler ,31897,2.10,2.10,2.10,2.10,true ,360906451,[Senior Python Developer,Data Scientist,Accountant,Internship],[true,true,true],[5.94],[5],[5],[5.94] +1953-01-07T00:00:00Z,10067,Claudi ,M,1987-03-04T00:00:00Z,2,2,2,2,Stavenow ,52044,1.77,1.77,1.77,1.77,true ,347664141,[Tech Lead,Principal Support Engineer],[false,false],[8.72,4.44],[8,4],[8,4],[8.72,4.44] +1962-11-26T00:00:00Z,10068,Charlene ,M,1987-08-07T00:00:00Z,3,3,3,3,Brattka ,28941,1.58,1.58,1.58,1.58,true ,233999584,[Architect],[true],[3.43,-5.61,-5.29],[3,-5,-5],[3,-5,-5],[3.43,-5.61,-5.29] +1960-09-06T00:00:00Z,10069,Margareta ,F,1989-11-05T00:00:00Z,5,5,5,5,Bierman ,41933,1.77,1.77,1.77,1.77,true ,366512352,[Business Analyst,Junior Developer,Purchase Manager,Support Engineer],[false],[-3.34,-6.33,6.23,-0.31],[-3,-6,6,0],[-3,-6,6,0],[-3.34,-6.33,6.23,-0.31] +1955-08-20T00:00:00Z,10070,Reuven ,M,1985-10-14T00:00:00Z,3,3,3,3,Garigliano ,54329,1.77,1.77,1.77,1.77,true ,347188604,[],[true,true,true],[-5.90],[-5],[-5],[-5.90] +1958-01-21T00:00:00Z,10071,Hisao ,M,1987-10-01T00:00:00Z,2,2,2,2,Lipner ,40612,2.07,2.07,2.07,2.07,false,306671693,[Business Analyst,Reporting Analyst,Senior Team Lead],[false,false,false],[-2.69],[-2],[-2],[-2.69] +1952-05-15T00:00:00Z,10072,Hironoby ,F,1988-07-21T00:00:00Z,5,5,5,5,Sidou ,54518,1.82,1.82,1.82,1.82,true ,209506065,[Architect,Tech Lead,Python Developer,Senior Team Lead],[false,false,true,false],[11.21,-2.30,2.22,-5.44],[11,-2,2,-5],[11,-2,2,-5],[11.21,-2.30,2.22,-5.44] +1954-02-23T00:00:00Z,10073,Shir ,M,1991-12-01T00:00:00Z,4,4,4,4,McClurg ,32568,1.66,1.66,1.66,1.66,false,314930367,[Principal Support Engineer,Python Developer,Junior Developer,Purchase Manager],[true,false],[-5.67],[-5],[-5],[-5.67] +1955-08-28T00:00:00Z,10074,Mokhtar ,F,1990-08-13T00:00:00Z,5,5,5,5,Bernatsky ,38992,1.64,1.64,1.64,1.64,true ,382397583,[Senior Python Developer,Python Developer],[true,false,false,true],[6.70,1.98,-5.64,2.96],[6,1,-5,2],[6,1,-5,2],[6.70,1.98,-5.64,2.96] +1960-03-09T00:00:00Z,10075,Gao ,F,1987-03-19T00:00:00Z,5,5,5,5,Dolinsky ,51956,1.94,1.94,1.94,1.94,false,370238919,[Purchase Manager],[true],[9.63,-3.29,8.42],[9,-3,8],[9,-3,8],[9.63,-3.29,8.42] +1952-06-13T00:00:00Z,10076,Erez ,F,1985-07-09T00:00:00Z,3,3,3,3,Ritzmann ,62405,1.83,1.83,1.83,1.83,false,376240317,[Architect,Senior Python Developer],[false],[-6.90,-1.30,8.75],[-6,-1,8],[-6,-1,8],[-6.90,-1.30,8.75] +1964-04-18T00:00:00Z,10077,Mona ,M,1990-03-02T00:00:00Z,5,5,5,5,Azuma ,46595,1.68,1.68,1.68,1.68,false,351960222,[Internship],[],[-0.01],[0],[0],[-0.01] +1959-12-25T00:00:00Z,10078,Danel ,F,1987-05-26T00:00:00Z,2,2,2,2,Mondadori ,69904,1.81,1.81,1.81,1.81,true ,377116038,[Architect,Principal Support Engineer,Internship],[true],[-7.88,9.98,12.52],[-7,9,12],[-7,9,12],[-7.88,9.98,12.52] +1961-10-05T00:00:00Z,10079,Kshitij ,F,1986-03-27T00:00:00Z,2,2,2,2,Gils ,32263,1.59,1.59,1.59,1.59,false,320953330,[],[false],[7.58],[7],[7],[7.58] +1957-12-03T00:00:00Z,10080,Premal ,M,1985-11-19T00:00:00Z,5,5,5,5,Baek ,52833,1.80,1.80,1.80,1.80,false,239266137,[Senior Python Developer],[],[-4.35,7.36,5.56],[-4,7,5],[-4,7,5],[-4.35,7.36,5.56] +1960-12-17T00:00:00Z,10081,Zhongwei ,M,1986-10-30T00:00:00Z,2,2,2,2,Rosen ,50128,1.44,1.44,1.44,1.44,true ,321375511,[Accountant,Internship],[false,false,false],[],[],[],[] +1963-09-09T00:00:00Z,10082,Parviz ,M,1990-01-03T00:00:00Z,4,4,4,4,Lortz ,49818,1.61,1.61,1.61,1.61,false,232522994,[Principal Support Engineer],[false],[1.19,-3.39],[1,-3],[1,-3],[1.19,-3.39] +1959-07-23T00:00:00Z,10083,Vishv ,M,1987-03-31T00:00:00Z,1,1,1,1,Zockler ,39110,1.42,1.42,1.42,1.42,false,331236443,[Head Human Resources],[],[],[],[],[] +1960-05-25T00:00:00Z,10084,Tuval ,M,1995-12-15T00:00:00Z,1,1,1,1,Kalloufi ,28035,1.51,1.51,1.51,1.51,true ,359067056,[Principal Support Engineer],[false],[],[],[],[] +1962-11-07T00:00:00Z,10085,Kenroku ,M,1994-04-09T00:00:00Z,5,5,5,5,Malabarba ,35742,2.01,2.01,2.01,2.01,true ,353404008,[Senior Python Developer,Business Analyst,Tech Lead,Accountant],[],[11.67,6.75,8.40],[11,6,8],[11,6,8],[11.67,6.75,8.40] +1962-11-19T00:00:00Z,10086,Somnath ,M,1990-02-16T00:00:00Z,1,1,1,1,Foote ,68547,1.74,1.74,1.74,1.74,true ,328580163,[Senior Python Developer],[false,true],[13.61],[13],[13],[13.61] +1959-07-23T00:00:00Z,10087,Xinglin ,F,1986-09-08T00:00:00Z,5,5,5,5,Eugenio ,32272,1.74,1.74,1.74,1.74,true ,305782871,[Junior Developer,Internship],[false,false],[-2.05],[-2],[-2],[-2.05] +1954-02-25T00:00:00Z,10088,Jungsoon ,F,1988-09-02T00:00:00Z,5,5,5,5,Syrzycki ,39638,1.91,1.91,1.91,1.91,false,330714423,[Reporting Analyst,Business Analyst,Tech Lead],[true],[],[],[],[] +1963-03-21T00:00:00Z,10089,Sudharsan ,F,1986-08-12T00:00:00Z,4,4,4,4,Flasterstein,43602,1.57,1.57,1.57,1.57,true ,232951673,[Junior Developer,Accountant],[true,false,false,false],[],[],[],[] +1961-05-30T00:00:00Z,10090,Kendra ,M,1986-03-14T00:00:00Z,2,2,2,2,Hofting ,44956,2.03,2.03,2.03,2.03,true ,212460105,[],[false,false,false,true],[7.15,-1.85,3.60],[7,-1,3],[7,-1,3],[7.15,-1.85,3.60] +1955-10-04T00:00:00Z,10091,Amabile ,M,1992-11-18T00:00:00Z,3,3,3,3,Gomatam ,38645,2.09,2.09,2.09,2.09,true ,242582807,[Reporting Analyst,Python Developer],[true,true,false,false],[-9.23,7.50,5.85,5.19],[-9,7,5,5],[-9,7,5,5],[-9.23,7.50,5.85,5.19] +1964-10-18T00:00:00Z,10092,Valdiodio ,F,1989-09-22T00:00:00Z,1,1,1,1,Niizuma ,25976,1.75,1.75,1.75,1.75,false,313407352,[Junior Developer,Accountant],[false,false,true,true],[8.78,0.39,-6.77,8.30],[8,0,-6,8],[8,0,-6,8],[8.78,0.39,-6.77,8.30] +1964-06-11T00:00:00Z,10093,Sailaja ,M,1996-11-05T00:00:00Z,3,3,3,3,Desikan ,45656,1.69,1.69,1.69,1.69,false,315904921,[Reporting Analyst,Tech Lead,Principal Support Engineer,Purchase Manager],[],[-0.88],[0],[0],[-0.88] +1957-05-25T00:00:00Z,10094,Arumugam ,F,1987-04-18T00:00:00Z,5,5,5,5,Ossenbruggen,66817,2.10,2.10,2.10,2.10,false,332920135,[Senior Python Developer,Principal Support Engineer,Accountant],[true,false,true],[2.22,7.92],[2,7],[2,7],[2.22,7.92] +1965-01-03T00:00:00Z,10095,Hilari ,M,1986-07-15T00:00:00Z,4,4,4,4,Morton ,37702,1.55,1.55,1.55,1.55,false,321850475,[],[true,true,false,false],[-3.93,-6.66],[-3,-6],[-3,-6],[-3.93,-6.66] +1954-09-16T00:00:00Z,10096,Jayson ,M,1990-01-14T00:00:00Z,4,4,4,4,Mandell ,43889,1.94,1.94,1.94,1.94,false,204381503,[Architect,Reporting Analyst],[false,false,false],[],[],[],[] +1952-02-27T00:00:00Z,10097,Remzi ,M,1990-09-15T00:00:00Z,3,3,3,3,Waschkowski ,71165,1.53,1.53,1.53,1.53,false,206258084,[Reporting Analyst,Tech Lead],[true,false],[-1.12],[-1],[-1],[-1.12] +1961-09-23T00:00:00Z,10098,Sreekrishna,F,1985-05-13T00:00:00Z,4,4,4,4,Servieres ,44817,2.00,2.00,2.00,2.00,false,272392146,[Architect,Internship,Senior Team Lead],[false],[-2.83,8.31,4.38],[-2,8,4],[-2,8,4],[-2.83,8.31,4.38] +1956-05-25T00:00:00Z,10099,Valter ,F,1988-10-18T00:00:00Z,2,2,2,2,Sullins ,73578,1.81,1.81,1.81,1.81,true ,377713748,[],[true,true],[10.71,14.26,-8.78,-3.98],[10,14,-8,-3],[10,14,-8,-3],[10.71,14.26,-8.78,-3.98] +1953-04-21T00:00:00Z,10100,Hironobu ,F,1987-09-21T00:00:00Z,4,4,4,4,Haraldson ,68431,1.77,1.77,1.77,1.77,true ,223910853,[Purchase Manager],[false,true,true,false],[13.97,-7.49],[13,-7],[13,-7],[13.97,-7.49] diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json index ee1ef56a63df..04b59f347ebf 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json @@ -17,6 +17,9 @@ "date": { "type": "date" }, + "date_nanos": { + "type": "date_nanos" + }, "double": { "type": "double" }, diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-basic.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-basic.json index 9ce87d01bfbb..85e797aea86d 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-basic.json +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-basic.json @@ -21,6 +21,9 @@ "_meta_field": { "type" : "keyword" }, + "hire_date": { + "type": "date" + }, "job": { "type": "text", "fields": { diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-default-incompatible.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-default-incompatible.json new file mode 100644 index 000000000000..607ae5c9ab2c --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-default-incompatible.json @@ -0,0 +1,80 @@ +{ + "properties" : { + "emp_no" : { + "type" : "long" + }, + "first_name" : { + "type" : "text" + }, + "last_name" : { + "type" : "text" + }, + "gender" : { + "type" : "text" + }, + "birth_date": { + "type" : "date" + }, + "hire_date": { + "type" : "date_nanos" + }, + "salary" : { + "type" : "long" + }, + "languages" : { + "type" : "byte", + "fields": { + "long": { + "type": "long" + }, + "short": { + "type": "short" + }, + "int": { + "type": "integer" + } + } + }, + "height": { + "type" : "float", + "fields" : { + "double" : { + "type" : "double" + }, + "scaled_float": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "half_float": { + "type": "half_float" + } + } + }, + "still_hired": { + "type" : "keyword" + }, + "avg_worked_seconds" : { + "type" : "unsigned_long" + }, + "job_positions" : { + "type" : "text" + }, + "is_rehired" : { + "type" : "keyword" + }, + "salary_change": { + "type": "float", + "fields": { + "int": { + "type": "integer" + }, + "long": { + "type": "long" + }, + "keyword": { + "type" : "keyword" + } + } + } + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec index c35f4c19cc34..03b24555dbef 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec @@ -197,3 +197,329 @@ emp_no:integer | first_name:keyword | last_name:keyword 10041 | Uri | Lenart 10043 | Yishay | Tzvieli ; + +testMatchBooleanField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(still_hired, true) and height > 2.08 +| keep first_name, still_hired, height; +ignoreOrder:true + +first_name:keyword | still_hired:boolean | height:double +Saniya | true | 2.1 +Yongqiao | true | 2.1 +Kwee | true | 2.1 +Amabile | true | 2.09 +; + +testMatchIntegerField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(emp_no, 10004) +| keep emp_no, first_name; + +emp_no:integer | first_name:keyword +10004 | Chirstian +; + +testMatchDoubleField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(salary_change, 9.07) +| keep emp_no, salary_change; + +emp_no:integer | salary_change:double +10014 | [-1.89, 9.07] +; + +testMatchLongField +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where match(num, 1698069301543123456) +| keep num; + +num:long +1698069301543123456 +; + +testMatchUnsignedLongField +required_capability: match_function +required_capability: match_additional_types + +from ul_logs +| where match(bytes_out, 12749081495402663265) +| keep bytes_out; + +bytes_out:unsigned_long +12749081495402663265 +; + +testMatchVersionField +required_capability: match_function +required_capability: match_additional_types + +from apps +| where match(version, "2.1"::VERSION) +| keep name, version; + +name:keyword | version:version +bbbbb | 2.1 +; + +testMatchIpField +required_capability: match_function +required_capability: match_additional_types + +from sample_data +| where match(client_ip, "172.21.0.5") +| keep client_ip, message; + +client_ip:ip | message:keyword +172.21.0.5 | Disconnected +; + +testMatchDateFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where match(millis, "2023-10-23T13:55:01.543Z") +| keep millis; + +millis:date +2023-10-23T13:55:01.543Z +; + +testMatchDateNanosFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where match(nanos, "2023-10-23T13:55:01.543123456Z") +| keep nanos; + +nanos:date_nanos +2023-10-23T13:55:01.543123456Z +; + +testMatchBooleanFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(still_hired, "true") and height > 2.08 +| keep first_name, still_hired, height; +ignoreOrder:true + +first_name:keyword | still_hired:boolean | height:double +Saniya | true | 2.1 +Yongqiao | true | 2.1 +Kwee | true | 2.1 +Amabile | true | 2.09 +; + +testMatchIntegerFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(emp_no, "10004") +| keep emp_no, first_name; + +emp_no:integer | first_name:keyword +10004 | Chirstian +; + +testMatchDoubleFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(salary_change, "9.07") +| keep emp_no, salary_change; + +emp_no:integer | salary_change:double +10014 | [-1.89, 9.07] +; + +testMatchLongFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where match(num, "1698069301543123456") +| keep num; + +num:long +1698069301543123456 +; + +testMatchUnsignedLongFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from ul_logs +| where match(bytes_out, "12749081495402663265") +| keep bytes_out; + +bytes_out:unsigned_long +12749081495402663265 +; + +testMatchVersionFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from apps +| where match(version, "2.1") +| keep name, version; + +name:keyword | version:version +bbbbb | 2.1 +; + +testMatchIntegerAsDouble +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(emp_no, 10004.0) +| keep emp_no, first_name; +ignoreOrder:true + +emp_no:integer | first_name:keyword +10004 | Chirstian +; + +testMatchDoubleAsIntegerField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where match(height, 2) +| keep emp_no, height; +ignoreOrder:true + +emp_no:integer | height:double +10037 | 2.0 +10048 | 2.0 +10098 | 2.0 +; + +testMatchMultipleFieldTypesIntLong +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(emp_no::int, 10005) +| eval emp_as_int = emp_no::int +| eval name_as_kw = first_name::keyword +| keep emp_as_int, name_as_kw +; + +emp_as_int:integer | name_as_kw:keyword +10005 | Kyoichi +10005 | Kyoichi +; + +testMatchMultipleFieldTypesKeywordText +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(first_name::keyword, "Kazuhito") +| eval first_name_kwd = first_name::keyword +| keep first_name_kwd +; + +first_name_kwd:keyword +Kazuhito +Kazuhito +; + +testMatchMultipleFieldTypesDoubleFloat +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(height::double, 2.03) +| eval height_dbl = height::double +| eval emp_no = emp_no::int +| keep emp_no, height_dbl +; +ignoreOrder:true + +emp_no:integer | height_dbl:double +10001 | 2.0299999713897705 +10090 | 2.0299999713897705 +10001 | 2.03 +10090 | 2.03 +; + +testMatchMultipleFieldTypesBooleanKeyword +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(still_hired::keyword, "true") and height.scaled_float == 2.08 +| eval still_hired_bool = still_hired::boolean +| keep still_hired_bool +; + +still_hired_bool:boolean +true +true +true +true +; + +testMatchMultipleFieldTypesLongUnsignedLong +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(avg_worked_seconds::unsigned_long, 200296405) +| eval avg_worked_seconds_ul = avg_worked_seconds::unsigned_long +| keep avg_worked_seconds_ul +; + +avg_worked_seconds_ul:unsigned_long +200296405 +200296405 +; + +testMatchMultipleFieldTypesDateNanosDate +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(hire_date::datetime, "1986-06-26T00:00:00.000Z") +| eval hire_date_nanos = hire_date::date_nanos +| keep hire_date_nanos +; + +hire_date_nanos:date_nanos +1986-06-26T00:00:00.000Z +1986-06-26T00:00:00.000Z +; + +testMatchWithWrongFieldValue +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where match(still_hired::boolean, "Wrong boolean") +| eval emp_no_bool = emp_no::boolean +| keep emp_no_bool +; + +emp_no_bool:boolean +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec index 7b55ece964b8..56f7f5ccd882 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-operator.csv-spec @@ -217,3 +217,319 @@ count(*): long | author.keyword:keyword 1 | Paul Faulkner 8 | William Faulkner ; + +testMatchBooleanField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where still_hired:true and height > 2.08 +| keep first_name, still_hired, height; +ignoreOrder:true + +first_name:keyword | still_hired:boolean | height:double +Saniya | true | 2.1 +Yongqiao | true | 2.1 +Kwee | true | 2.1 +Amabile | true | 2.09 +; + +testMatchIntegerField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where emp_no:10004 +| keep emp_no, first_name; + +emp_no:integer | first_name:keyword +10004 | Chirstian +; + +testMatchDoubleField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where salary_change:9.07 +| keep emp_no, salary_change; + +emp_no:integer | salary_change:double +10014 | [-1.89, 9.07] +; + +testMatchLongField +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where num:1698069301543123456 +| keep num; + +num:long +1698069301543123456 +; + +testMatchUnsignedLongField +required_capability: match_function +required_capability: match_additional_types + +from ul_logs +| where bytes_out:12749081495402663265 +| keep bytes_out; + +bytes_out:unsigned_long +12749081495402663265 +; + +testMatchIpFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from sample_data +| where client_ip:"172.21.0.5" +| keep client_ip, message; + +client_ip:ip | message:keyword +172.21.0.5 | Disconnected +; + +testMatchDateFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where millis:"2023-10-23T13:55:01.543Z" +| keep millis; + +millis:date +2023-10-23T13:55:01.543Z +; + +testMatchDateNanosFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where nanos:"2023-10-23T13:55:01.543123456Z" +| keep nanos; + +nanos:date_nanos +2023-10-23T13:55:01.543123456Z +; + +testMatchBooleanFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from employees +| where still_hired:"true" and height > 2.08 +| keep first_name, still_hired, height; +ignoreOrder:true + +first_name:keyword | still_hired:boolean | height:double +Saniya | true | 2.1 +Yongqiao | true | 2.1 +Kwee | true | 2.1 +Amabile | true | 2.09 +; + +testMatchIntegerFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from employees +| where emp_no:"10004" +| keep emp_no, first_name; + +emp_no:integer | first_name:keyword +10004 | Chirstian +; + +testMatchDoubleFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from employees +| where salary_change:"9.07" +| keep emp_no, salary_change; + +emp_no:integer | salary_change:double +10014 | [-1.89, 9.07] +; + +testMatchLongFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from date_nanos +| where num:"1698069301543123456" +| keep num; + +num:long +1698069301543123456 +; + +testMatchUnsignedLongFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from ul_logs +| where bytes_out:"12749081495402663265" +| keep bytes_out; + +bytes_out:unsigned_long +12749081495402663265 +; + +testMatchVersionFieldAsString +required_capability: match_function +required_capability: match_additional_types + +from apps +| where version:"2.1" +| keep name, version; + +name:keyword | version:version +bbbbb | 2.1 +; + +testMatchIntegerAsDouble +required_capability: match_function +required_capability: match_additional_types + +from employees +| where emp_no:10004.0 +| keep emp_no, first_name; +ignoreOrder:true + +emp_no:integer | first_name:keyword +10004 | Chirstian +; + +testMatchDoubleAsIntegerField +required_capability: match_function +required_capability: match_additional_types + +from employees +| where height:2 +| keep emp_no, height; +ignoreOrder:true + +emp_no:integer | height:double +10037 | 2.0 +10048 | 2.0 +10098 | 2.0 +; + +testMatchMultipleFieldTypes +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where emp_no::int : 10005 +| eval emp_as_int = emp_no::int +| eval name_as_kw = first_name::keyword +| keep emp_as_int, name_as_kw +; + +emp_as_int:integer | name_as_kw:keyword +10005 | Kyoichi +10005 | Kyoichi +; + + +testMatchMultipleFieldTypesKeywordText +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where first_name::keyword : "Kazuhito" +| eval first_name_kwd = first_name::keyword +| keep first_name_kwd +; + +first_name_kwd:keyword +Kazuhito +Kazuhito +; + +testMatchMultipleFieldTypesDoubleFloat +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where height::double : 2.03 +| eval height_dbl = height::double +| eval emp_no = emp_no::int +| keep emp_no, height_dbl +; +ignoreOrder:true + +emp_no:integer | height_dbl:double +10001 | 2.0299999713897705 +10090 | 2.0299999713897705 +10001 | 2.03 +10090 | 2.03 +; + +testMatchMultipleFieldTypesBooleanKeyword +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where still_hired::keyword : "true" and height.scaled_float == 2.08 +| eval still_hired_bool = still_hired::boolean +| keep still_hired_bool +; + +still_hired_bool:boolean +true +true +true +true +; + +testMatchMultipleFieldTypesLongUnsignedLong +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where avg_worked_seconds::unsigned_long : 200296405 +| eval avg_worked_seconds_ul = avg_worked_seconds::unsigned_long +| keep avg_worked_seconds_ul +; + +avg_worked_seconds_ul:unsigned_long +200296405 +200296405 +; + +testMatchMultipleFieldTypesDateNanosDate +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where hire_date::datetime : "1986-06-26T00:00:00.000Z" +| eval hire_date_nanos = hire_date::date_nanos +| keep hire_date_nanos +; + +hire_date_nanos:date_nanos +1986-06-26T00:00:00.000Z +1986-06-26T00:00:00.000Z +; + +testMatchWithWrongFieldValue +required_capability: match_function +required_capability: match_additional_types + +from employees,employees_incompatible +| where still_hired::boolean : "Wrong boolean" +| eval emp_no_bool = emp_no::boolean +| keep emp_no_bool +; + +emp_no_bool:boolean +; + diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java index 99f7d48a0d63..58b1652653ca 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java @@ -268,16 +268,6 @@ public void testMatchWithinEval() { assertThat(error.getMessage(), containsString("[:] operator is only supported in WHERE commands")); } - public void testMatchWithNonTextField() { - var query = """ - FROM test - | WHERE id:"fox" - """; - - var error = expectThrows(VerificationException.class, () -> run(query)); - assertThat(error.getMessage(), containsString("first argument of [id:\"fox\"] must be [string], found value [id] type [integer]")); - } - private void createAndPopulateIndex() { var indexName = "test"; var client = client().admin().indices(); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java index 6a360eb319ab..d0a641f086fe 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java @@ -11,7 +11,6 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.AbstractEsqlIntegTestCase; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; @@ -22,7 +21,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.CoreMatchers.containsString; -@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE,org.elasticsearch.compute:TRACE", reason = "debug") +//@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE,org.elasticsearch.compute:TRACE", reason = "debug") public class MatchOperatorIT extends AbstractEsqlIntegTestCase { @Before @@ -248,11 +247,15 @@ public void testMatchWithinEval() { public void testMatchWithNonTextField() { var query = """ FROM test - | WHERE id:"fox" + | WHERE id:3 + | KEEP id """; - var error = expectThrows(VerificationException.class, () -> run(query)); - assertThat(error.getMessage(), containsString("first argument of [id:\"fox\"] must be [string], found value [id] type [integer]")); + try (var resp = run(query)) { + assertColumnNames(resp.columns(), List.of("id")); + assertColumnTypes(resp.columns(), List.of("integer")); + assertValues(resp.values(), List.of(List.of(3))); + } } private void createAndPopulateIndex() { diff --git a/x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4 b/x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4 index f84cfe306050..efc2e3660990 100644 --- a/x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4 +++ b/x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4 @@ -78,7 +78,7 @@ regexBooleanExpression ; matchBooleanExpression - : fieldExp=qualifiedName COLON queryString=constant + : fieldExp=qualifiedName (CAST_OP fieldType=dataType)? COLON matchQuery=constant ; valueExpression diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 7c3f2a45df6a..1aee1df3dbaf 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -560,7 +560,12 @@ public enum Cap { /** * Term function */ - TERM_FUNCTION(Build.current().isSnapshot()); + TERM_FUNCTION(Build.current().isSnapshot()), + + /** + * Additional types for match function and operator + */ + MATCH_ADDITIONAL_TYPES; private final boolean enabled; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java index 9addf08e1b5f..78dc05af8f34 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java @@ -17,7 +17,6 @@ import java.util.List; -import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; @@ -47,7 +46,16 @@ protected final TypeResolution resolveType() { return new TypeResolution("Unresolved children"); } - return resolveNonQueryParamTypes().and(resolveQueryParamType()); + return resolveNonQueryParamTypes().and(resolveQueryParamType().and(checkParamCompatibility())); + } + + /** + * Checks parameter specific compatibility, to be overriden by subclasses + * + * @return TypeResolution for param compatibility + */ + protected TypeResolution checkParamCompatibility() { + return TypeResolution.TYPE_RESOLVED; } /** @@ -55,7 +63,7 @@ protected final TypeResolution resolveType() { * * @return type resolution for query parameter */ - private TypeResolution resolveQueryParamType() { + protected TypeResolution resolveQueryParamType() { return isString(query(), sourceText(), queryParamOrdinal()).and(isNotNullAndFoldable(query(), sourceText(), queryParamOrdinal())); } @@ -73,19 +81,17 @@ public Expression query() { } /** - * Returns the resulting query as a String + * Returns the resulting query as an object * - * @return query expression as a string + * @return query expression as an object */ - public final String queryAsText() { + public Object queryAsObject() { Object queryAsObject = query().fold(); if (queryAsObject instanceof BytesRef bytesRef) { return bytesRef.utf8ToString(); } - throw new IllegalArgumentException( - format(null, "{} argument in {} function needs to be resolved to a string", queryParamOrdinal(), functionName()) - ); + return queryAsObject; } /** diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 522a5574c005..2b9a7c73a585 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -19,19 +20,37 @@ import org.elasticsearch.xpack.esql.core.querydsl.query.QueryStringQuery; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; +import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; import java.io.IOException; import java.util.List; import java.util.Locale; +import java.util.Set; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; +import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; +import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; +import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; +import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; +import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; +import static org.elasticsearch.xpack.esql.core.type.DataType.IP; +import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; +import static org.elasticsearch.xpack.esql.core.type.DataType.LONG; +import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; +import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; +import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; +import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage; /** * Full text function that performs a {@link QueryStringQuery} . @@ -44,19 +63,50 @@ public class Match extends FullTextFunction implements Validatable { private transient Boolean isOperator; + public static final Set FIELD_DATA_TYPES = Set.of( + KEYWORD, + TEXT, + BOOLEAN, + DATETIME, + DATE_NANOS, + DOUBLE, + INTEGER, + IP, + LONG, + UNSIGNED_LONG, + VERSION + ); + public static final Set QUERY_DATA_TYPES = Set.of( + KEYWORD, + BOOLEAN, + DATETIME, + DATE_NANOS, + DOUBLE, + INTEGER, + IP, + LONG, + UNSIGNED_LONG, + VERSION + ); + @FunctionInfo( returnType = "boolean", preview = true, - description = "Performs a match query on the specified field. Returns true if the provided query matches the row.", + description = "Performs a <> on the specified field. " + + "Returns true if the provided query matches the row.", examples = { @Example(file = "match-function", tag = "match-with-field") } ) public Match( Source source, - @Param(name = "field", type = { "keyword", "text" }, description = "Field that the query will target.") Expression field, + @Param( + name = "field", + type = { "keyword", "text", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, + description = "Field that the query will target." + ) Expression field, @Param( name = "query", - type = { "keyword", "text" }, - description = "Text you wish to find in the provided field." + type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, + description = "Value to find in the provided field." ) Expression matchQuery ) { super(source, matchQuery, List.of(field, matchQuery)); @@ -84,12 +134,56 @@ public String getWriteableName() { @Override protected TypeResolution resolveNonQueryParamTypes() { - return isNotNull(field, sourceText(), FIRST).and(isString(field, sourceText(), FIRST)).and(super.resolveNonQueryParamTypes()); + return isNotNull(field, sourceText(), FIRST).and( + isType( + field, + FIELD_DATA_TYPES::contains, + sourceText(), + FIRST, + "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" + ) + ); + } + + @Override + protected TypeResolution resolveQueryParamType() { + return isType( + query(), + QUERY_DATA_TYPES::contains, + sourceText(), + queryParamOrdinal(), + "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" + ).and(isNotNullAndFoldable(query(), sourceText(), queryParamOrdinal())); + } + + @Override + protected TypeResolution checkParamCompatibility() { + DataType fieldType = field().dataType(); + DataType queryType = query().dataType(); + + // Field and query types should match. If the query is a string, then it can match any field type. + if ((fieldType == queryType) || (queryType == KEYWORD)) { + return TypeResolution.TYPE_RESOLVED; + } + + if (fieldType.isNumeric() && queryType.isNumeric()) { + // When doing an unsigned long query, field must be an unsigned long + if ((queryType == UNSIGNED_LONG && fieldType != UNSIGNED_LONG) == false) { + return TypeResolution.TYPE_RESOLVED; + } + } + + return new TypeResolution(formatIncompatibleTypesMessage(fieldType, queryType, sourceText())); } @Override public void validate(Failures failures) { - if (field instanceof FieldAttribute == false) { + Expression fieldExpression = field(); + // Field may be converted to other data type (field_name :: data_type), so we need to check the original field + if (fieldExpression instanceof AbstractConvertFunction convertFunction) { + fieldExpression = convertFunction.field(); + } + if (fieldExpression instanceof FieldAttribute == false) { failures.add( Failure.fail( field, @@ -102,6 +196,32 @@ public void validate(Failures failures) { } } + @Override + public Object queryAsObject() { + Object queryAsObject = query().fold(); + + // Convert BytesRef to string for string-based values + if (queryAsObject instanceof BytesRef bytesRef) { + return switch (query().dataType()) { + case IP -> EsqlDataTypeConverter.ipToString(bytesRef); + case VERSION -> EsqlDataTypeConverter.versionToString(bytesRef); + default -> bytesRef.utf8ToString(); + }; + } + + // Converts specific types to the correct type for the query + if (query().dataType() == DataType.UNSIGNED_LONG) { + return NumericUtils.unsignedLongAsBigInteger((Long) queryAsObject); + } else if (query().dataType() == DataType.DATETIME && queryAsObject instanceof Long) { + // When casting to date and datetime, we get a long back. But Match query needs a date string + return EsqlDataTypeConverter.dateTimeToString((Long) queryAsObject); + } else if (query().dataType() == DATE_NANOS && queryAsObject instanceof Long) { + return EsqlDataTypeConverter.nanoTimeToString((Long) queryAsObject); + } + + return queryAsObject; + } + @Override public Expression replaceChildren(List newChildren) { return new Match(source(), newChildren.get(0), newChildren.get(1)); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java index 0d7d15a13dd8..bd79661534b7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java @@ -32,7 +32,8 @@ public class QueryString extends FullTextFunction { @FunctionInfo( returnType = "boolean", preview = true, - description = "Performs a query string query. Returns true if the provided query string matches the row.", + description = "Performs a <>. " + + "Returns true if the provided query string matches the row.", examples = { @Example(file = "qstr-function", tag = "qstr-with-field") } ) public QueryString( diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/EsqlBinaryComparison.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/EsqlBinaryComparison.java index 217c6528c9fd..3e2a21664aa7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/EsqlBinaryComparison.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/EsqlBinaryComparison.java @@ -243,7 +243,7 @@ protected TypeResolution checkCompatibility() { // Unsigned long is only interoperable with other unsigned longs if ((rightType == UNSIGNED_LONG && (false == (leftType == UNSIGNED_LONG || leftType == DataType.NULL))) || (leftType == UNSIGNED_LONG && (false == (rightType == UNSIGNED_LONG || rightType == DataType.NULL)))) { - return new TypeResolution(formatIncompatibleTypesMessage()); + return new TypeResolution(formatIncompatibleTypesMessage(left().dataType(), right().dataType(), sourceText())); } if ((leftType.isNumeric() && rightType.isNumeric()) @@ -254,35 +254,35 @@ protected TypeResolution checkCompatibility() { || DataType.isNull(rightType)) { return TypeResolution.TYPE_RESOLVED; } - return new TypeResolution(formatIncompatibleTypesMessage()); + return new TypeResolution(formatIncompatibleTypesMessage(left().dataType(), right().dataType(), sourceText())); } - public String formatIncompatibleTypesMessage() { - if (left().dataType().equals(UNSIGNED_LONG)) { + public static String formatIncompatibleTypesMessage(DataType leftType, DataType rightType, String sourceText) { + if (leftType.equals(UNSIGNED_LONG)) { return format( null, "first argument of [{}] is [unsigned_long] and second is [{}]. " + "[unsigned_long] can only be operated on together with another [unsigned_long]", - sourceText(), - right().dataType().typeName() + sourceText, + rightType.typeName() ); } - if (right().dataType().equals(UNSIGNED_LONG)) { + if (rightType.equals(UNSIGNED_LONG)) { return format( null, "first argument of [{}] is [{}] and second is [unsigned_long]. " + "[unsigned_long] can only be operated on together with another [unsigned_long]", - sourceText(), - left().dataType().typeName() + sourceText, + leftType.typeName() ); } return format( null, "first argument of [{}] is [{}] so second argument must also be [{}] but was [{}]", - sourceText(), - left().dataType().isNumeric() ? "numeric" : left().dataType().typeName(), - left().dataType().isNumeric() ? "numeric" : left().dataType().typeName(), - right().dataType().typeName() + sourceText, + leftType.isNumeric() ? "numeric" : leftType.typeName(), + leftType.isNumeric() ? "numeric" : leftType.typeName(), + rightType.typeName() ); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushFiltersToSource.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushFiltersToSource.java index 9d02af0efbab..6fcdd538fdfc 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushFiltersToSource.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushFiltersToSource.java @@ -31,7 +31,6 @@ import org.elasticsearch.xpack.esql.core.util.CollectionUtils; import org.elasticsearch.xpack.esql.core.util.Queries; import org.elasticsearch.xpack.esql.expression.function.fulltext.FullTextFunction; -import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; import org.elasticsearch.xpack.esql.expression.function.fulltext.Term; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.BinarySpatialFunction; @@ -253,8 +252,6 @@ static boolean canPushToSource(Expression exp, LucenePushdownPredicates lucenePu && Expressions.foldable(cidrMatch.matches()); } else if (exp instanceof SpatialRelatesFunction spatial) { return canPushSpatialFunctionToSource(spatial, lucenePushdownPredicates); - } else if (exp instanceof Match mf) { - return mf.field() instanceof FieldAttribute && DataType.isString(mf.field().dataType()); } else if (exp instanceof Term term) { return term.field() instanceof FieldAttribute && DataType.isString(term.field().dataType()); } else if (exp instanceof FullTextFunction) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp index 50493f584fe4..c5b37fa411f6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp @@ -330,4 +330,4 @@ joinPredicate atn: -[4, 1, 128, 635, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 142, 8, 1, 10, 1, 12, 1, 145, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 153, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 173, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 185, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 192, 8, 5, 10, 5, 12, 5, 195, 9, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 202, 8, 5, 1, 5, 1, 5, 1, 5, 3, 5, 207, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 215, 8, 5, 10, 5, 12, 5, 218, 9, 5, 1, 6, 1, 6, 3, 6, 222, 8, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 229, 8, 6, 1, 6, 1, 6, 1, 6, 3, 6, 234, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 245, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 251, 8, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 5, 9, 259, 8, 9, 10, 9, 12, 9, 262, 9, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 3, 10, 272, 8, 10, 1, 10, 1, 10, 1, 10, 5, 10, 277, 8, 10, 10, 10, 12, 10, 280, 9, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 5, 11, 288, 8, 11, 10, 11, 12, 11, 291, 9, 11, 3, 11, 293, 8, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 307, 8, 15, 10, 15, 12, 15, 310, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 315, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 323, 8, 17, 10, 17, 12, 17, 326, 9, 17, 1, 17, 3, 17, 329, 8, 17, 1, 18, 1, 18, 1, 18, 3, 18, 334, 8, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 1, 20, 1, 21, 1, 21, 3, 21, 344, 8, 21, 1, 22, 1, 22, 1, 22, 1, 22, 5, 22, 350, 8, 22, 10, 22, 12, 22, 353, 9, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 363, 8, 24, 10, 24, 12, 24, 366, 9, 24, 1, 24, 3, 24, 369, 8, 24, 1, 24, 1, 24, 3, 24, 373, 8, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 3, 26, 380, 8, 26, 1, 26, 1, 26, 3, 26, 384, 8, 26, 1, 27, 1, 27, 1, 27, 5, 27, 389, 8, 27, 10, 27, 12, 27, 392, 9, 27, 1, 28, 1, 28, 1, 28, 3, 28, 397, 8, 28, 1, 29, 1, 29, 1, 29, 5, 29, 402, 8, 29, 10, 29, 12, 29, 405, 9, 29, 1, 30, 1, 30, 1, 30, 5, 30, 410, 8, 30, 10, 30, 12, 30, 413, 9, 30, 1, 31, 1, 31, 1, 31, 5, 31, 418, 8, 31, 10, 31, 12, 31, 421, 9, 31, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 3, 33, 428, 8, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 443, 8, 34, 10, 34, 12, 34, 446, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 454, 8, 34, 10, 34, 12, 34, 457, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 465, 8, 34, 10, 34, 12, 34, 468, 9, 34, 1, 34, 1, 34, 3, 34, 472, 8, 34, 1, 35, 1, 35, 3, 35, 476, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 481, 8, 36, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 490, 8, 38, 10, 38, 12, 38, 493, 9, 38, 1, 39, 1, 39, 3, 39, 497, 8, 39, 1, 39, 1, 39, 3, 39, 501, 8, 39, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 5, 42, 513, 8, 42, 10, 42, 12, 42, 516, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 526, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 5, 47, 538, 8, 47, 10, 47, 12, 47, 541, 9, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 50, 1, 50, 3, 50, 551, 8, 50, 1, 51, 3, 51, 554, 8, 51, 1, 51, 1, 51, 1, 52, 3, 52, 559, 8, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 581, 8, 58, 1, 58, 1, 58, 1, 58, 1, 58, 5, 58, 587, 8, 58, 10, 58, 12, 58, 590, 9, 58, 3, 58, 592, 8, 58, 1, 59, 1, 59, 1, 59, 3, 59, 597, 8, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 610, 8, 61, 1, 62, 3, 62, 613, 8, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 3, 63, 622, 8, 63, 1, 64, 1, 64, 1, 64, 1, 64, 5, 64, 628, 8, 64, 10, 64, 12, 64, 631, 9, 64, 1, 65, 1, 65, 1, 65, 0, 4, 2, 10, 18, 20, 66, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 0, 9, 1, 0, 64, 65, 1, 0, 66, 68, 2, 0, 30, 30, 81, 81, 1, 0, 72, 73, 2, 0, 35, 35, 40, 40, 2, 0, 43, 43, 46, 46, 2, 0, 42, 42, 56, 56, 2, 0, 57, 57, 59, 63, 1, 0, 22, 24, 660, 0, 132, 1, 0, 0, 0, 2, 135, 1, 0, 0, 0, 4, 152, 1, 0, 0, 0, 6, 172, 1, 0, 0, 0, 8, 174, 1, 0, 0, 0, 10, 206, 1, 0, 0, 0, 12, 233, 1, 0, 0, 0, 14, 235, 1, 0, 0, 0, 16, 244, 1, 0, 0, 0, 18, 250, 1, 0, 0, 0, 20, 271, 1, 0, 0, 0, 22, 281, 1, 0, 0, 0, 24, 296, 1, 0, 0, 0, 26, 298, 1, 0, 0, 0, 28, 300, 1, 0, 0, 0, 30, 303, 1, 0, 0, 0, 32, 314, 1, 0, 0, 0, 34, 318, 1, 0, 0, 0, 36, 333, 1, 0, 0, 0, 38, 337, 1, 0, 0, 0, 40, 339, 1, 0, 0, 0, 42, 343, 1, 0, 0, 0, 44, 345, 1, 0, 0, 0, 46, 354, 1, 0, 0, 0, 48, 358, 1, 0, 0, 0, 50, 374, 1, 0, 0, 0, 52, 377, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 393, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 406, 1, 0, 0, 0, 62, 414, 1, 0, 0, 0, 64, 422, 1, 0, 0, 0, 66, 427, 1, 0, 0, 0, 68, 471, 1, 0, 0, 0, 70, 475, 1, 0, 0, 0, 72, 480, 1, 0, 0, 0, 74, 482, 1, 0, 0, 0, 76, 485, 1, 0, 0, 0, 78, 494, 1, 0, 0, 0, 80, 502, 1, 0, 0, 0, 82, 505, 1, 0, 0, 0, 84, 508, 1, 0, 0, 0, 86, 517, 1, 0, 0, 0, 88, 521, 1, 0, 0, 0, 90, 527, 1, 0, 0, 0, 92, 531, 1, 0, 0, 0, 94, 534, 1, 0, 0, 0, 96, 542, 1, 0, 0, 0, 98, 546, 1, 0, 0, 0, 100, 550, 1, 0, 0, 0, 102, 553, 1, 0, 0, 0, 104, 558, 1, 0, 0, 0, 106, 562, 1, 0, 0, 0, 108, 564, 1, 0, 0, 0, 110, 566, 1, 0, 0, 0, 112, 569, 1, 0, 0, 0, 114, 573, 1, 0, 0, 0, 116, 576, 1, 0, 0, 0, 118, 596, 1, 0, 0, 0, 120, 600, 1, 0, 0, 0, 122, 605, 1, 0, 0, 0, 124, 612, 1, 0, 0, 0, 126, 618, 1, 0, 0, 0, 128, 623, 1, 0, 0, 0, 130, 632, 1, 0, 0, 0, 132, 133, 3, 2, 1, 0, 133, 134, 5, 0, 0, 1, 134, 1, 1, 0, 0, 0, 135, 136, 6, 1, -1, 0, 136, 137, 3, 4, 2, 0, 137, 143, 1, 0, 0, 0, 138, 139, 10, 1, 0, 0, 139, 140, 5, 29, 0, 0, 140, 142, 3, 6, 3, 0, 141, 138, 1, 0, 0, 0, 142, 145, 1, 0, 0, 0, 143, 141, 1, 0, 0, 0, 143, 144, 1, 0, 0, 0, 144, 3, 1, 0, 0, 0, 145, 143, 1, 0, 0, 0, 146, 153, 3, 110, 55, 0, 147, 153, 3, 34, 17, 0, 148, 153, 3, 28, 14, 0, 149, 153, 3, 114, 57, 0, 150, 151, 4, 2, 1, 0, 151, 153, 3, 48, 24, 0, 152, 146, 1, 0, 0, 0, 152, 147, 1, 0, 0, 0, 152, 148, 1, 0, 0, 0, 152, 149, 1, 0, 0, 0, 152, 150, 1, 0, 0, 0, 153, 5, 1, 0, 0, 0, 154, 173, 3, 50, 25, 0, 155, 173, 3, 8, 4, 0, 156, 173, 3, 80, 40, 0, 157, 173, 3, 74, 37, 0, 158, 173, 3, 52, 26, 0, 159, 173, 3, 76, 38, 0, 160, 173, 3, 82, 41, 0, 161, 173, 3, 84, 42, 0, 162, 173, 3, 88, 44, 0, 163, 173, 3, 90, 45, 0, 164, 173, 3, 116, 58, 0, 165, 173, 3, 92, 46, 0, 166, 167, 4, 3, 2, 0, 167, 173, 3, 122, 61, 0, 168, 169, 4, 3, 3, 0, 169, 173, 3, 120, 60, 0, 170, 171, 4, 3, 4, 0, 171, 173, 3, 124, 62, 0, 172, 154, 1, 0, 0, 0, 172, 155, 1, 0, 0, 0, 172, 156, 1, 0, 0, 0, 172, 157, 1, 0, 0, 0, 172, 158, 1, 0, 0, 0, 172, 159, 1, 0, 0, 0, 172, 160, 1, 0, 0, 0, 172, 161, 1, 0, 0, 0, 172, 162, 1, 0, 0, 0, 172, 163, 1, 0, 0, 0, 172, 164, 1, 0, 0, 0, 172, 165, 1, 0, 0, 0, 172, 166, 1, 0, 0, 0, 172, 168, 1, 0, 0, 0, 172, 170, 1, 0, 0, 0, 173, 7, 1, 0, 0, 0, 174, 175, 5, 16, 0, 0, 175, 176, 3, 10, 5, 0, 176, 9, 1, 0, 0, 0, 177, 178, 6, 5, -1, 0, 178, 179, 5, 49, 0, 0, 179, 207, 3, 10, 5, 8, 180, 207, 3, 16, 8, 0, 181, 207, 3, 12, 6, 0, 182, 184, 3, 16, 8, 0, 183, 185, 5, 49, 0, 0, 184, 183, 1, 0, 0, 0, 184, 185, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 5, 44, 0, 0, 187, 188, 5, 48, 0, 0, 188, 193, 3, 16, 8, 0, 189, 190, 5, 39, 0, 0, 190, 192, 3, 16, 8, 0, 191, 189, 1, 0, 0, 0, 192, 195, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0, 194, 196, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 196, 197, 5, 55, 0, 0, 197, 207, 1, 0, 0, 0, 198, 199, 3, 16, 8, 0, 199, 201, 5, 45, 0, 0, 200, 202, 5, 49, 0, 0, 201, 200, 1, 0, 0, 0, 201, 202, 1, 0, 0, 0, 202, 203, 1, 0, 0, 0, 203, 204, 5, 50, 0, 0, 204, 207, 1, 0, 0, 0, 205, 207, 3, 14, 7, 0, 206, 177, 1, 0, 0, 0, 206, 180, 1, 0, 0, 0, 206, 181, 1, 0, 0, 0, 206, 182, 1, 0, 0, 0, 206, 198, 1, 0, 0, 0, 206, 205, 1, 0, 0, 0, 207, 216, 1, 0, 0, 0, 208, 209, 10, 5, 0, 0, 209, 210, 5, 34, 0, 0, 210, 215, 3, 10, 5, 6, 211, 212, 10, 4, 0, 0, 212, 213, 5, 52, 0, 0, 213, 215, 3, 10, 5, 5, 214, 208, 1, 0, 0, 0, 214, 211, 1, 0, 0, 0, 215, 218, 1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 11, 1, 0, 0, 0, 218, 216, 1, 0, 0, 0, 219, 221, 3, 16, 8, 0, 220, 222, 5, 49, 0, 0, 221, 220, 1, 0, 0, 0, 221, 222, 1, 0, 0, 0, 222, 223, 1, 0, 0, 0, 223, 224, 5, 47, 0, 0, 224, 225, 3, 106, 53, 0, 225, 234, 1, 0, 0, 0, 226, 228, 3, 16, 8, 0, 227, 229, 5, 49, 0, 0, 228, 227, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 230, 1, 0, 0, 0, 230, 231, 5, 54, 0, 0, 231, 232, 3, 106, 53, 0, 232, 234, 1, 0, 0, 0, 233, 219, 1, 0, 0, 0, 233, 226, 1, 0, 0, 0, 234, 13, 1, 0, 0, 0, 235, 236, 3, 58, 29, 0, 236, 237, 5, 38, 0, 0, 237, 238, 3, 68, 34, 0, 238, 15, 1, 0, 0, 0, 239, 245, 3, 18, 9, 0, 240, 241, 3, 18, 9, 0, 241, 242, 3, 108, 54, 0, 242, 243, 3, 18, 9, 0, 243, 245, 1, 0, 0, 0, 244, 239, 1, 0, 0, 0, 244, 240, 1, 0, 0, 0, 245, 17, 1, 0, 0, 0, 246, 247, 6, 9, -1, 0, 247, 251, 3, 20, 10, 0, 248, 249, 7, 0, 0, 0, 249, 251, 3, 18, 9, 3, 250, 246, 1, 0, 0, 0, 250, 248, 1, 0, 0, 0, 251, 260, 1, 0, 0, 0, 252, 253, 10, 2, 0, 0, 253, 254, 7, 1, 0, 0, 254, 259, 3, 18, 9, 3, 255, 256, 10, 1, 0, 0, 256, 257, 7, 0, 0, 0, 257, 259, 3, 18, 9, 2, 258, 252, 1, 0, 0, 0, 258, 255, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 19, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 264, 6, 10, -1, 0, 264, 272, 3, 68, 34, 0, 265, 272, 3, 58, 29, 0, 266, 272, 3, 22, 11, 0, 267, 268, 5, 48, 0, 0, 268, 269, 3, 10, 5, 0, 269, 270, 5, 55, 0, 0, 270, 272, 1, 0, 0, 0, 271, 263, 1, 0, 0, 0, 271, 265, 1, 0, 0, 0, 271, 266, 1, 0, 0, 0, 271, 267, 1, 0, 0, 0, 272, 278, 1, 0, 0, 0, 273, 274, 10, 1, 0, 0, 274, 275, 5, 37, 0, 0, 275, 277, 3, 26, 13, 0, 276, 273, 1, 0, 0, 0, 277, 280, 1, 0, 0, 0, 278, 276, 1, 0, 0, 0, 278, 279, 1, 0, 0, 0, 279, 21, 1, 0, 0, 0, 280, 278, 1, 0, 0, 0, 281, 282, 3, 24, 12, 0, 282, 292, 5, 48, 0, 0, 283, 293, 5, 66, 0, 0, 284, 289, 3, 10, 5, 0, 285, 286, 5, 39, 0, 0, 286, 288, 3, 10, 5, 0, 287, 285, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289, 290, 1, 0, 0, 0, 290, 293, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 283, 1, 0, 0, 0, 292, 284, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 5, 55, 0, 0, 295, 23, 1, 0, 0, 0, 296, 297, 3, 72, 36, 0, 297, 25, 1, 0, 0, 0, 298, 299, 3, 64, 32, 0, 299, 27, 1, 0, 0, 0, 300, 301, 5, 12, 0, 0, 301, 302, 3, 30, 15, 0, 302, 29, 1, 0, 0, 0, 303, 308, 3, 32, 16, 0, 304, 305, 5, 39, 0, 0, 305, 307, 3, 32, 16, 0, 306, 304, 1, 0, 0, 0, 307, 310, 1, 0, 0, 0, 308, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 31, 1, 0, 0, 0, 310, 308, 1, 0, 0, 0, 311, 312, 3, 58, 29, 0, 312, 313, 5, 36, 0, 0, 313, 315, 1, 0, 0, 0, 314, 311, 1, 0, 0, 0, 314, 315, 1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 317, 3, 10, 5, 0, 317, 33, 1, 0, 0, 0, 318, 319, 5, 6, 0, 0, 319, 324, 3, 36, 18, 0, 320, 321, 5, 39, 0, 0, 321, 323, 3, 36, 18, 0, 322, 320, 1, 0, 0, 0, 323, 326, 1, 0, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 328, 1, 0, 0, 0, 326, 324, 1, 0, 0, 0, 327, 329, 3, 42, 21, 0, 328, 327, 1, 0, 0, 0, 328, 329, 1, 0, 0, 0, 329, 35, 1, 0, 0, 0, 330, 331, 3, 38, 19, 0, 331, 332, 5, 38, 0, 0, 332, 334, 1, 0, 0, 0, 333, 330, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 335, 1, 0, 0, 0, 335, 336, 3, 40, 20, 0, 336, 37, 1, 0, 0, 0, 337, 338, 5, 81, 0, 0, 338, 39, 1, 0, 0, 0, 339, 340, 7, 2, 0, 0, 340, 41, 1, 0, 0, 0, 341, 344, 3, 44, 22, 0, 342, 344, 3, 46, 23, 0, 343, 341, 1, 0, 0, 0, 343, 342, 1, 0, 0, 0, 344, 43, 1, 0, 0, 0, 345, 346, 5, 80, 0, 0, 346, 351, 5, 81, 0, 0, 347, 348, 5, 39, 0, 0, 348, 350, 5, 81, 0, 0, 349, 347, 1, 0, 0, 0, 350, 353, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, 45, 1, 0, 0, 0, 353, 351, 1, 0, 0, 0, 354, 355, 5, 70, 0, 0, 355, 356, 3, 44, 22, 0, 356, 357, 5, 71, 0, 0, 357, 47, 1, 0, 0, 0, 358, 359, 5, 19, 0, 0, 359, 364, 3, 36, 18, 0, 360, 361, 5, 39, 0, 0, 361, 363, 3, 36, 18, 0, 362, 360, 1, 0, 0, 0, 363, 366, 1, 0, 0, 0, 364, 362, 1, 0, 0, 0, 364, 365, 1, 0, 0, 0, 365, 368, 1, 0, 0, 0, 366, 364, 1, 0, 0, 0, 367, 369, 3, 54, 27, 0, 368, 367, 1, 0, 0, 0, 368, 369, 1, 0, 0, 0, 369, 372, 1, 0, 0, 0, 370, 371, 5, 33, 0, 0, 371, 373, 3, 30, 15, 0, 372, 370, 1, 0, 0, 0, 372, 373, 1, 0, 0, 0, 373, 49, 1, 0, 0, 0, 374, 375, 5, 4, 0, 0, 375, 376, 3, 30, 15, 0, 376, 51, 1, 0, 0, 0, 377, 379, 5, 15, 0, 0, 378, 380, 3, 54, 27, 0, 379, 378, 1, 0, 0, 0, 379, 380, 1, 0, 0, 0, 380, 383, 1, 0, 0, 0, 381, 382, 5, 33, 0, 0, 382, 384, 3, 30, 15, 0, 383, 381, 1, 0, 0, 0, 383, 384, 1, 0, 0, 0, 384, 53, 1, 0, 0, 0, 385, 390, 3, 56, 28, 0, 386, 387, 5, 39, 0, 0, 387, 389, 3, 56, 28, 0, 388, 386, 1, 0, 0, 0, 389, 392, 1, 0, 0, 0, 390, 388, 1, 0, 0, 0, 390, 391, 1, 0, 0, 0, 391, 55, 1, 0, 0, 0, 392, 390, 1, 0, 0, 0, 393, 396, 3, 32, 16, 0, 394, 395, 5, 16, 0, 0, 395, 397, 3, 10, 5, 0, 396, 394, 1, 0, 0, 0, 396, 397, 1, 0, 0, 0, 397, 57, 1, 0, 0, 0, 398, 403, 3, 72, 36, 0, 399, 400, 5, 41, 0, 0, 400, 402, 3, 72, 36, 0, 401, 399, 1, 0, 0, 0, 402, 405, 1, 0, 0, 0, 403, 401, 1, 0, 0, 0, 403, 404, 1, 0, 0, 0, 404, 59, 1, 0, 0, 0, 405, 403, 1, 0, 0, 0, 406, 411, 3, 66, 33, 0, 407, 408, 5, 41, 0, 0, 408, 410, 3, 66, 33, 0, 409, 407, 1, 0, 0, 0, 410, 413, 1, 0, 0, 0, 411, 409, 1, 0, 0, 0, 411, 412, 1, 0, 0, 0, 412, 61, 1, 0, 0, 0, 413, 411, 1, 0, 0, 0, 414, 419, 3, 60, 30, 0, 415, 416, 5, 39, 0, 0, 416, 418, 3, 60, 30, 0, 417, 415, 1, 0, 0, 0, 418, 421, 1, 0, 0, 0, 419, 417, 1, 0, 0, 0, 419, 420, 1, 0, 0, 0, 420, 63, 1, 0, 0, 0, 421, 419, 1, 0, 0, 0, 422, 423, 7, 3, 0, 0, 423, 65, 1, 0, 0, 0, 424, 428, 5, 85, 0, 0, 425, 426, 4, 33, 10, 0, 426, 428, 3, 70, 35, 0, 427, 424, 1, 0, 0, 0, 427, 425, 1, 0, 0, 0, 428, 67, 1, 0, 0, 0, 429, 472, 5, 50, 0, 0, 430, 431, 3, 104, 52, 0, 431, 432, 5, 72, 0, 0, 432, 472, 1, 0, 0, 0, 433, 472, 3, 102, 51, 0, 434, 472, 3, 104, 52, 0, 435, 472, 3, 98, 49, 0, 436, 472, 3, 70, 35, 0, 437, 472, 3, 106, 53, 0, 438, 439, 5, 70, 0, 0, 439, 444, 3, 100, 50, 0, 440, 441, 5, 39, 0, 0, 441, 443, 3, 100, 50, 0, 442, 440, 1, 0, 0, 0, 443, 446, 1, 0, 0, 0, 444, 442, 1, 0, 0, 0, 444, 445, 1, 0, 0, 0, 445, 447, 1, 0, 0, 0, 446, 444, 1, 0, 0, 0, 447, 448, 5, 71, 0, 0, 448, 472, 1, 0, 0, 0, 449, 450, 5, 70, 0, 0, 450, 455, 3, 98, 49, 0, 451, 452, 5, 39, 0, 0, 452, 454, 3, 98, 49, 0, 453, 451, 1, 0, 0, 0, 454, 457, 1, 0, 0, 0, 455, 453, 1, 0, 0, 0, 455, 456, 1, 0, 0, 0, 456, 458, 1, 0, 0, 0, 457, 455, 1, 0, 0, 0, 458, 459, 5, 71, 0, 0, 459, 472, 1, 0, 0, 0, 460, 461, 5, 70, 0, 0, 461, 466, 3, 106, 53, 0, 462, 463, 5, 39, 0, 0, 463, 465, 3, 106, 53, 0, 464, 462, 1, 0, 0, 0, 465, 468, 1, 0, 0, 0, 466, 464, 1, 0, 0, 0, 466, 467, 1, 0, 0, 0, 467, 469, 1, 0, 0, 0, 468, 466, 1, 0, 0, 0, 469, 470, 5, 71, 0, 0, 470, 472, 1, 0, 0, 0, 471, 429, 1, 0, 0, 0, 471, 430, 1, 0, 0, 0, 471, 433, 1, 0, 0, 0, 471, 434, 1, 0, 0, 0, 471, 435, 1, 0, 0, 0, 471, 436, 1, 0, 0, 0, 471, 437, 1, 0, 0, 0, 471, 438, 1, 0, 0, 0, 471, 449, 1, 0, 0, 0, 471, 460, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 476, 5, 53, 0, 0, 474, 476, 5, 69, 0, 0, 475, 473, 1, 0, 0, 0, 475, 474, 1, 0, 0, 0, 476, 71, 1, 0, 0, 0, 477, 481, 3, 64, 32, 0, 478, 479, 4, 36, 11, 0, 479, 481, 3, 70, 35, 0, 480, 477, 1, 0, 0, 0, 480, 478, 1, 0, 0, 0, 481, 73, 1, 0, 0, 0, 482, 483, 5, 9, 0, 0, 483, 484, 5, 31, 0, 0, 484, 75, 1, 0, 0, 0, 485, 486, 5, 14, 0, 0, 486, 491, 3, 78, 39, 0, 487, 488, 5, 39, 0, 0, 488, 490, 3, 78, 39, 0, 489, 487, 1, 0, 0, 0, 490, 493, 1, 0, 0, 0, 491, 489, 1, 0, 0, 0, 491, 492, 1, 0, 0, 0, 492, 77, 1, 0, 0, 0, 493, 491, 1, 0, 0, 0, 494, 496, 3, 10, 5, 0, 495, 497, 7, 4, 0, 0, 496, 495, 1, 0, 0, 0, 496, 497, 1, 0, 0, 0, 497, 500, 1, 0, 0, 0, 498, 499, 5, 51, 0, 0, 499, 501, 7, 5, 0, 0, 500, 498, 1, 0, 0, 0, 500, 501, 1, 0, 0, 0, 501, 79, 1, 0, 0, 0, 502, 503, 5, 8, 0, 0, 503, 504, 3, 62, 31, 0, 504, 81, 1, 0, 0, 0, 505, 506, 5, 2, 0, 0, 506, 507, 3, 62, 31, 0, 507, 83, 1, 0, 0, 0, 508, 509, 5, 11, 0, 0, 509, 514, 3, 86, 43, 0, 510, 511, 5, 39, 0, 0, 511, 513, 3, 86, 43, 0, 512, 510, 1, 0, 0, 0, 513, 516, 1, 0, 0, 0, 514, 512, 1, 0, 0, 0, 514, 515, 1, 0, 0, 0, 515, 85, 1, 0, 0, 0, 516, 514, 1, 0, 0, 0, 517, 518, 3, 60, 30, 0, 518, 519, 5, 89, 0, 0, 519, 520, 3, 60, 30, 0, 520, 87, 1, 0, 0, 0, 521, 522, 5, 1, 0, 0, 522, 523, 3, 20, 10, 0, 523, 525, 3, 106, 53, 0, 524, 526, 3, 94, 47, 0, 525, 524, 1, 0, 0, 0, 525, 526, 1, 0, 0, 0, 526, 89, 1, 0, 0, 0, 527, 528, 5, 7, 0, 0, 528, 529, 3, 20, 10, 0, 529, 530, 3, 106, 53, 0, 530, 91, 1, 0, 0, 0, 531, 532, 5, 10, 0, 0, 532, 533, 3, 58, 29, 0, 533, 93, 1, 0, 0, 0, 534, 539, 3, 96, 48, 0, 535, 536, 5, 39, 0, 0, 536, 538, 3, 96, 48, 0, 537, 535, 1, 0, 0, 0, 538, 541, 1, 0, 0, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 95, 1, 0, 0, 0, 541, 539, 1, 0, 0, 0, 542, 543, 3, 64, 32, 0, 543, 544, 5, 36, 0, 0, 544, 545, 3, 68, 34, 0, 545, 97, 1, 0, 0, 0, 546, 547, 7, 6, 0, 0, 547, 99, 1, 0, 0, 0, 548, 551, 3, 102, 51, 0, 549, 551, 3, 104, 52, 0, 550, 548, 1, 0, 0, 0, 550, 549, 1, 0, 0, 0, 551, 101, 1, 0, 0, 0, 552, 554, 7, 0, 0, 0, 553, 552, 1, 0, 0, 0, 553, 554, 1, 0, 0, 0, 554, 555, 1, 0, 0, 0, 555, 556, 5, 32, 0, 0, 556, 103, 1, 0, 0, 0, 557, 559, 7, 0, 0, 0, 558, 557, 1, 0, 0, 0, 558, 559, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 561, 5, 31, 0, 0, 561, 105, 1, 0, 0, 0, 562, 563, 5, 30, 0, 0, 563, 107, 1, 0, 0, 0, 564, 565, 7, 7, 0, 0, 565, 109, 1, 0, 0, 0, 566, 567, 5, 5, 0, 0, 567, 568, 3, 112, 56, 0, 568, 111, 1, 0, 0, 0, 569, 570, 5, 70, 0, 0, 570, 571, 3, 2, 1, 0, 571, 572, 5, 71, 0, 0, 572, 113, 1, 0, 0, 0, 573, 574, 5, 13, 0, 0, 574, 575, 5, 105, 0, 0, 575, 115, 1, 0, 0, 0, 576, 577, 5, 3, 0, 0, 577, 580, 5, 95, 0, 0, 578, 579, 5, 93, 0, 0, 579, 581, 3, 60, 30, 0, 580, 578, 1, 0, 0, 0, 580, 581, 1, 0, 0, 0, 581, 591, 1, 0, 0, 0, 582, 583, 5, 94, 0, 0, 583, 588, 3, 118, 59, 0, 584, 585, 5, 39, 0, 0, 585, 587, 3, 118, 59, 0, 586, 584, 1, 0, 0, 0, 587, 590, 1, 0, 0, 0, 588, 586, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 592, 1, 0, 0, 0, 590, 588, 1, 0, 0, 0, 591, 582, 1, 0, 0, 0, 591, 592, 1, 0, 0, 0, 592, 117, 1, 0, 0, 0, 593, 594, 3, 60, 30, 0, 594, 595, 5, 36, 0, 0, 595, 597, 1, 0, 0, 0, 596, 593, 1, 0, 0, 0, 596, 597, 1, 0, 0, 0, 597, 598, 1, 0, 0, 0, 598, 599, 3, 60, 30, 0, 599, 119, 1, 0, 0, 0, 600, 601, 5, 18, 0, 0, 601, 602, 3, 36, 18, 0, 602, 603, 5, 93, 0, 0, 603, 604, 3, 62, 31, 0, 604, 121, 1, 0, 0, 0, 605, 606, 5, 17, 0, 0, 606, 609, 3, 54, 27, 0, 607, 608, 5, 33, 0, 0, 608, 610, 3, 30, 15, 0, 609, 607, 1, 0, 0, 0, 609, 610, 1, 0, 0, 0, 610, 123, 1, 0, 0, 0, 611, 613, 7, 8, 0, 0, 612, 611, 1, 0, 0, 0, 612, 613, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 615, 5, 20, 0, 0, 615, 616, 3, 126, 63, 0, 616, 617, 3, 128, 64, 0, 617, 125, 1, 0, 0, 0, 618, 621, 3, 64, 32, 0, 619, 620, 5, 89, 0, 0, 620, 622, 3, 64, 32, 0, 621, 619, 1, 0, 0, 0, 621, 622, 1, 0, 0, 0, 622, 127, 1, 0, 0, 0, 623, 624, 5, 93, 0, 0, 624, 629, 3, 130, 65, 0, 625, 626, 5, 39, 0, 0, 626, 628, 3, 130, 65, 0, 627, 625, 1, 0, 0, 0, 628, 631, 1, 0, 0, 0, 629, 627, 1, 0, 0, 0, 629, 630, 1, 0, 0, 0, 630, 129, 1, 0, 0, 0, 631, 629, 1, 0, 0, 0, 632, 633, 3, 16, 8, 0, 633, 131, 1, 0, 0, 0, 61, 143, 152, 172, 184, 193, 201, 206, 214, 216, 221, 228, 233, 244, 250, 258, 260, 271, 278, 289, 292, 308, 314, 324, 328, 333, 343, 351, 364, 368, 372, 379, 383, 390, 396, 403, 411, 419, 427, 444, 455, 466, 471, 475, 480, 491, 496, 500, 514, 525, 539, 550, 553, 558, 580, 588, 591, 596, 609, 612, 621, 629] \ No newline at end of file +[4, 1, 128, 639, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 142, 8, 1, 10, 1, 12, 1, 145, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 153, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 173, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 185, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 192, 8, 5, 10, 5, 12, 5, 195, 9, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 202, 8, 5, 1, 5, 1, 5, 1, 5, 3, 5, 207, 8, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 5, 5, 215, 8, 5, 10, 5, 12, 5, 218, 9, 5, 1, 6, 1, 6, 3, 6, 222, 8, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 229, 8, 6, 1, 6, 1, 6, 1, 6, 3, 6, 234, 8, 6, 1, 7, 1, 7, 1, 7, 3, 7, 239, 8, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 249, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 255, 8, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 5, 9, 263, 8, 9, 10, 9, 12, 9, 266, 9, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 3, 10, 276, 8, 10, 1, 10, 1, 10, 1, 10, 5, 10, 281, 8, 10, 10, 10, 12, 10, 284, 9, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 5, 11, 292, 8, 11, 10, 11, 12, 11, 295, 9, 11, 3, 11, 297, 8, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 311, 8, 15, 10, 15, 12, 15, 314, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 319, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 327, 8, 17, 10, 17, 12, 17, 330, 9, 17, 1, 17, 3, 17, 333, 8, 17, 1, 18, 1, 18, 1, 18, 3, 18, 338, 8, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 20, 1, 20, 1, 21, 1, 21, 3, 21, 348, 8, 21, 1, 22, 1, 22, 1, 22, 1, 22, 5, 22, 354, 8, 22, 10, 22, 12, 22, 357, 9, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 367, 8, 24, 10, 24, 12, 24, 370, 9, 24, 1, 24, 3, 24, 373, 8, 24, 1, 24, 1, 24, 3, 24, 377, 8, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 3, 26, 384, 8, 26, 1, 26, 1, 26, 3, 26, 388, 8, 26, 1, 27, 1, 27, 1, 27, 5, 27, 393, 8, 27, 10, 27, 12, 27, 396, 9, 27, 1, 28, 1, 28, 1, 28, 3, 28, 401, 8, 28, 1, 29, 1, 29, 1, 29, 5, 29, 406, 8, 29, 10, 29, 12, 29, 409, 9, 29, 1, 30, 1, 30, 1, 30, 5, 30, 414, 8, 30, 10, 30, 12, 30, 417, 9, 30, 1, 31, 1, 31, 1, 31, 5, 31, 422, 8, 31, 10, 31, 12, 31, 425, 9, 31, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 3, 33, 432, 8, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 447, 8, 34, 10, 34, 12, 34, 450, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 458, 8, 34, 10, 34, 12, 34, 461, 9, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 469, 8, 34, 10, 34, 12, 34, 472, 9, 34, 1, 34, 1, 34, 3, 34, 476, 8, 34, 1, 35, 1, 35, 3, 35, 480, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 485, 8, 36, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 494, 8, 38, 10, 38, 12, 38, 497, 9, 38, 1, 39, 1, 39, 3, 39, 501, 8, 39, 1, 39, 1, 39, 3, 39, 505, 8, 39, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 5, 42, 517, 8, 42, 10, 42, 12, 42, 520, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 530, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 5, 47, 542, 8, 47, 10, 47, 12, 47, 545, 9, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 50, 1, 50, 3, 50, 555, 8, 50, 1, 51, 3, 51, 558, 8, 51, 1, 51, 1, 51, 1, 52, 3, 52, 563, 8, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 585, 8, 58, 1, 58, 1, 58, 1, 58, 1, 58, 5, 58, 591, 8, 58, 10, 58, 12, 58, 594, 9, 58, 3, 58, 596, 8, 58, 1, 59, 1, 59, 1, 59, 3, 59, 601, 8, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 614, 8, 61, 1, 62, 3, 62, 617, 8, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 3, 63, 626, 8, 63, 1, 64, 1, 64, 1, 64, 1, 64, 5, 64, 632, 8, 64, 10, 64, 12, 64, 635, 9, 64, 1, 65, 1, 65, 1, 65, 0, 4, 2, 10, 18, 20, 66, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 0, 9, 1, 0, 64, 65, 1, 0, 66, 68, 2, 0, 30, 30, 81, 81, 1, 0, 72, 73, 2, 0, 35, 35, 40, 40, 2, 0, 43, 43, 46, 46, 2, 0, 42, 42, 56, 56, 2, 0, 57, 57, 59, 63, 1, 0, 22, 24, 665, 0, 132, 1, 0, 0, 0, 2, 135, 1, 0, 0, 0, 4, 152, 1, 0, 0, 0, 6, 172, 1, 0, 0, 0, 8, 174, 1, 0, 0, 0, 10, 206, 1, 0, 0, 0, 12, 233, 1, 0, 0, 0, 14, 235, 1, 0, 0, 0, 16, 248, 1, 0, 0, 0, 18, 254, 1, 0, 0, 0, 20, 275, 1, 0, 0, 0, 22, 285, 1, 0, 0, 0, 24, 300, 1, 0, 0, 0, 26, 302, 1, 0, 0, 0, 28, 304, 1, 0, 0, 0, 30, 307, 1, 0, 0, 0, 32, 318, 1, 0, 0, 0, 34, 322, 1, 0, 0, 0, 36, 337, 1, 0, 0, 0, 38, 341, 1, 0, 0, 0, 40, 343, 1, 0, 0, 0, 42, 347, 1, 0, 0, 0, 44, 349, 1, 0, 0, 0, 46, 358, 1, 0, 0, 0, 48, 362, 1, 0, 0, 0, 50, 378, 1, 0, 0, 0, 52, 381, 1, 0, 0, 0, 54, 389, 1, 0, 0, 0, 56, 397, 1, 0, 0, 0, 58, 402, 1, 0, 0, 0, 60, 410, 1, 0, 0, 0, 62, 418, 1, 0, 0, 0, 64, 426, 1, 0, 0, 0, 66, 431, 1, 0, 0, 0, 68, 475, 1, 0, 0, 0, 70, 479, 1, 0, 0, 0, 72, 484, 1, 0, 0, 0, 74, 486, 1, 0, 0, 0, 76, 489, 1, 0, 0, 0, 78, 498, 1, 0, 0, 0, 80, 506, 1, 0, 0, 0, 82, 509, 1, 0, 0, 0, 84, 512, 1, 0, 0, 0, 86, 521, 1, 0, 0, 0, 88, 525, 1, 0, 0, 0, 90, 531, 1, 0, 0, 0, 92, 535, 1, 0, 0, 0, 94, 538, 1, 0, 0, 0, 96, 546, 1, 0, 0, 0, 98, 550, 1, 0, 0, 0, 100, 554, 1, 0, 0, 0, 102, 557, 1, 0, 0, 0, 104, 562, 1, 0, 0, 0, 106, 566, 1, 0, 0, 0, 108, 568, 1, 0, 0, 0, 110, 570, 1, 0, 0, 0, 112, 573, 1, 0, 0, 0, 114, 577, 1, 0, 0, 0, 116, 580, 1, 0, 0, 0, 118, 600, 1, 0, 0, 0, 120, 604, 1, 0, 0, 0, 122, 609, 1, 0, 0, 0, 124, 616, 1, 0, 0, 0, 126, 622, 1, 0, 0, 0, 128, 627, 1, 0, 0, 0, 130, 636, 1, 0, 0, 0, 132, 133, 3, 2, 1, 0, 133, 134, 5, 0, 0, 1, 134, 1, 1, 0, 0, 0, 135, 136, 6, 1, -1, 0, 136, 137, 3, 4, 2, 0, 137, 143, 1, 0, 0, 0, 138, 139, 10, 1, 0, 0, 139, 140, 5, 29, 0, 0, 140, 142, 3, 6, 3, 0, 141, 138, 1, 0, 0, 0, 142, 145, 1, 0, 0, 0, 143, 141, 1, 0, 0, 0, 143, 144, 1, 0, 0, 0, 144, 3, 1, 0, 0, 0, 145, 143, 1, 0, 0, 0, 146, 153, 3, 110, 55, 0, 147, 153, 3, 34, 17, 0, 148, 153, 3, 28, 14, 0, 149, 153, 3, 114, 57, 0, 150, 151, 4, 2, 1, 0, 151, 153, 3, 48, 24, 0, 152, 146, 1, 0, 0, 0, 152, 147, 1, 0, 0, 0, 152, 148, 1, 0, 0, 0, 152, 149, 1, 0, 0, 0, 152, 150, 1, 0, 0, 0, 153, 5, 1, 0, 0, 0, 154, 173, 3, 50, 25, 0, 155, 173, 3, 8, 4, 0, 156, 173, 3, 80, 40, 0, 157, 173, 3, 74, 37, 0, 158, 173, 3, 52, 26, 0, 159, 173, 3, 76, 38, 0, 160, 173, 3, 82, 41, 0, 161, 173, 3, 84, 42, 0, 162, 173, 3, 88, 44, 0, 163, 173, 3, 90, 45, 0, 164, 173, 3, 116, 58, 0, 165, 173, 3, 92, 46, 0, 166, 167, 4, 3, 2, 0, 167, 173, 3, 122, 61, 0, 168, 169, 4, 3, 3, 0, 169, 173, 3, 120, 60, 0, 170, 171, 4, 3, 4, 0, 171, 173, 3, 124, 62, 0, 172, 154, 1, 0, 0, 0, 172, 155, 1, 0, 0, 0, 172, 156, 1, 0, 0, 0, 172, 157, 1, 0, 0, 0, 172, 158, 1, 0, 0, 0, 172, 159, 1, 0, 0, 0, 172, 160, 1, 0, 0, 0, 172, 161, 1, 0, 0, 0, 172, 162, 1, 0, 0, 0, 172, 163, 1, 0, 0, 0, 172, 164, 1, 0, 0, 0, 172, 165, 1, 0, 0, 0, 172, 166, 1, 0, 0, 0, 172, 168, 1, 0, 0, 0, 172, 170, 1, 0, 0, 0, 173, 7, 1, 0, 0, 0, 174, 175, 5, 16, 0, 0, 175, 176, 3, 10, 5, 0, 176, 9, 1, 0, 0, 0, 177, 178, 6, 5, -1, 0, 178, 179, 5, 49, 0, 0, 179, 207, 3, 10, 5, 8, 180, 207, 3, 16, 8, 0, 181, 207, 3, 12, 6, 0, 182, 184, 3, 16, 8, 0, 183, 185, 5, 49, 0, 0, 184, 183, 1, 0, 0, 0, 184, 185, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 5, 44, 0, 0, 187, 188, 5, 48, 0, 0, 188, 193, 3, 16, 8, 0, 189, 190, 5, 39, 0, 0, 190, 192, 3, 16, 8, 0, 191, 189, 1, 0, 0, 0, 192, 195, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0, 194, 196, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 196, 197, 5, 55, 0, 0, 197, 207, 1, 0, 0, 0, 198, 199, 3, 16, 8, 0, 199, 201, 5, 45, 0, 0, 200, 202, 5, 49, 0, 0, 201, 200, 1, 0, 0, 0, 201, 202, 1, 0, 0, 0, 202, 203, 1, 0, 0, 0, 203, 204, 5, 50, 0, 0, 204, 207, 1, 0, 0, 0, 205, 207, 3, 14, 7, 0, 206, 177, 1, 0, 0, 0, 206, 180, 1, 0, 0, 0, 206, 181, 1, 0, 0, 0, 206, 182, 1, 0, 0, 0, 206, 198, 1, 0, 0, 0, 206, 205, 1, 0, 0, 0, 207, 216, 1, 0, 0, 0, 208, 209, 10, 5, 0, 0, 209, 210, 5, 34, 0, 0, 210, 215, 3, 10, 5, 6, 211, 212, 10, 4, 0, 0, 212, 213, 5, 52, 0, 0, 213, 215, 3, 10, 5, 5, 214, 208, 1, 0, 0, 0, 214, 211, 1, 0, 0, 0, 215, 218, 1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 11, 1, 0, 0, 0, 218, 216, 1, 0, 0, 0, 219, 221, 3, 16, 8, 0, 220, 222, 5, 49, 0, 0, 221, 220, 1, 0, 0, 0, 221, 222, 1, 0, 0, 0, 222, 223, 1, 0, 0, 0, 223, 224, 5, 47, 0, 0, 224, 225, 3, 106, 53, 0, 225, 234, 1, 0, 0, 0, 226, 228, 3, 16, 8, 0, 227, 229, 5, 49, 0, 0, 228, 227, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 230, 1, 0, 0, 0, 230, 231, 5, 54, 0, 0, 231, 232, 3, 106, 53, 0, 232, 234, 1, 0, 0, 0, 233, 219, 1, 0, 0, 0, 233, 226, 1, 0, 0, 0, 234, 13, 1, 0, 0, 0, 235, 238, 3, 58, 29, 0, 236, 237, 5, 37, 0, 0, 237, 239, 3, 26, 13, 0, 238, 236, 1, 0, 0, 0, 238, 239, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 241, 5, 38, 0, 0, 241, 242, 3, 68, 34, 0, 242, 15, 1, 0, 0, 0, 243, 249, 3, 18, 9, 0, 244, 245, 3, 18, 9, 0, 245, 246, 3, 108, 54, 0, 246, 247, 3, 18, 9, 0, 247, 249, 1, 0, 0, 0, 248, 243, 1, 0, 0, 0, 248, 244, 1, 0, 0, 0, 249, 17, 1, 0, 0, 0, 250, 251, 6, 9, -1, 0, 251, 255, 3, 20, 10, 0, 252, 253, 7, 0, 0, 0, 253, 255, 3, 18, 9, 3, 254, 250, 1, 0, 0, 0, 254, 252, 1, 0, 0, 0, 255, 264, 1, 0, 0, 0, 256, 257, 10, 2, 0, 0, 257, 258, 7, 1, 0, 0, 258, 263, 3, 18, 9, 3, 259, 260, 10, 1, 0, 0, 260, 261, 7, 0, 0, 0, 261, 263, 3, 18, 9, 2, 262, 256, 1, 0, 0, 0, 262, 259, 1, 0, 0, 0, 263, 266, 1, 0, 0, 0, 264, 262, 1, 0, 0, 0, 264, 265, 1, 0, 0, 0, 265, 19, 1, 0, 0, 0, 266, 264, 1, 0, 0, 0, 267, 268, 6, 10, -1, 0, 268, 276, 3, 68, 34, 0, 269, 276, 3, 58, 29, 0, 270, 276, 3, 22, 11, 0, 271, 272, 5, 48, 0, 0, 272, 273, 3, 10, 5, 0, 273, 274, 5, 55, 0, 0, 274, 276, 1, 0, 0, 0, 275, 267, 1, 0, 0, 0, 275, 269, 1, 0, 0, 0, 275, 270, 1, 0, 0, 0, 275, 271, 1, 0, 0, 0, 276, 282, 1, 0, 0, 0, 277, 278, 10, 1, 0, 0, 278, 279, 5, 37, 0, 0, 279, 281, 3, 26, 13, 0, 280, 277, 1, 0, 0, 0, 281, 284, 1, 0, 0, 0, 282, 280, 1, 0, 0, 0, 282, 283, 1, 0, 0, 0, 283, 21, 1, 0, 0, 0, 284, 282, 1, 0, 0, 0, 285, 286, 3, 24, 12, 0, 286, 296, 5, 48, 0, 0, 287, 297, 5, 66, 0, 0, 288, 293, 3, 10, 5, 0, 289, 290, 5, 39, 0, 0, 290, 292, 3, 10, 5, 0, 291, 289, 1, 0, 0, 0, 292, 295, 1, 0, 0, 0, 293, 291, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 297, 1, 0, 0, 0, 295, 293, 1, 0, 0, 0, 296, 287, 1, 0, 0, 0, 296, 288, 1, 0, 0, 0, 296, 297, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 299, 5, 55, 0, 0, 299, 23, 1, 0, 0, 0, 300, 301, 3, 72, 36, 0, 301, 25, 1, 0, 0, 0, 302, 303, 3, 64, 32, 0, 303, 27, 1, 0, 0, 0, 304, 305, 5, 12, 0, 0, 305, 306, 3, 30, 15, 0, 306, 29, 1, 0, 0, 0, 307, 312, 3, 32, 16, 0, 308, 309, 5, 39, 0, 0, 309, 311, 3, 32, 16, 0, 310, 308, 1, 0, 0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0, 313, 31, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 316, 3, 58, 29, 0, 316, 317, 5, 36, 0, 0, 317, 319, 1, 0, 0, 0, 318, 315, 1, 0, 0, 0, 318, 319, 1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 321, 3, 10, 5, 0, 321, 33, 1, 0, 0, 0, 322, 323, 5, 6, 0, 0, 323, 328, 3, 36, 18, 0, 324, 325, 5, 39, 0, 0, 325, 327, 3, 36, 18, 0, 326, 324, 1, 0, 0, 0, 327, 330, 1, 0, 0, 0, 328, 326, 1, 0, 0, 0, 328, 329, 1, 0, 0, 0, 329, 332, 1, 0, 0, 0, 330, 328, 1, 0, 0, 0, 331, 333, 3, 42, 21, 0, 332, 331, 1, 0, 0, 0, 332, 333, 1, 0, 0, 0, 333, 35, 1, 0, 0, 0, 334, 335, 3, 38, 19, 0, 335, 336, 5, 38, 0, 0, 336, 338, 1, 0, 0, 0, 337, 334, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 339, 1, 0, 0, 0, 339, 340, 3, 40, 20, 0, 340, 37, 1, 0, 0, 0, 341, 342, 5, 81, 0, 0, 342, 39, 1, 0, 0, 0, 343, 344, 7, 2, 0, 0, 344, 41, 1, 0, 0, 0, 345, 348, 3, 44, 22, 0, 346, 348, 3, 46, 23, 0, 347, 345, 1, 0, 0, 0, 347, 346, 1, 0, 0, 0, 348, 43, 1, 0, 0, 0, 349, 350, 5, 80, 0, 0, 350, 355, 5, 81, 0, 0, 351, 352, 5, 39, 0, 0, 352, 354, 5, 81, 0, 0, 353, 351, 1, 0, 0, 0, 354, 357, 1, 0, 0, 0, 355, 353, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 45, 1, 0, 0, 0, 357, 355, 1, 0, 0, 0, 358, 359, 5, 70, 0, 0, 359, 360, 3, 44, 22, 0, 360, 361, 5, 71, 0, 0, 361, 47, 1, 0, 0, 0, 362, 363, 5, 19, 0, 0, 363, 368, 3, 36, 18, 0, 364, 365, 5, 39, 0, 0, 365, 367, 3, 36, 18, 0, 366, 364, 1, 0, 0, 0, 367, 370, 1, 0, 0, 0, 368, 366, 1, 0, 0, 0, 368, 369, 1, 0, 0, 0, 369, 372, 1, 0, 0, 0, 370, 368, 1, 0, 0, 0, 371, 373, 3, 54, 27, 0, 372, 371, 1, 0, 0, 0, 372, 373, 1, 0, 0, 0, 373, 376, 1, 0, 0, 0, 374, 375, 5, 33, 0, 0, 375, 377, 3, 30, 15, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 379, 5, 4, 0, 0, 379, 380, 3, 30, 15, 0, 380, 51, 1, 0, 0, 0, 381, 383, 5, 15, 0, 0, 382, 384, 3, 54, 27, 0, 383, 382, 1, 0, 0, 0, 383, 384, 1, 0, 0, 0, 384, 387, 1, 0, 0, 0, 385, 386, 5, 33, 0, 0, 386, 388, 3, 30, 15, 0, 387, 385, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 53, 1, 0, 0, 0, 389, 394, 3, 56, 28, 0, 390, 391, 5, 39, 0, 0, 391, 393, 3, 56, 28, 0, 392, 390, 1, 0, 0, 0, 393, 396, 1, 0, 0, 0, 394, 392, 1, 0, 0, 0, 394, 395, 1, 0, 0, 0, 395, 55, 1, 0, 0, 0, 396, 394, 1, 0, 0, 0, 397, 400, 3, 32, 16, 0, 398, 399, 5, 16, 0, 0, 399, 401, 3, 10, 5, 0, 400, 398, 1, 0, 0, 0, 400, 401, 1, 0, 0, 0, 401, 57, 1, 0, 0, 0, 402, 407, 3, 72, 36, 0, 403, 404, 5, 41, 0, 0, 404, 406, 3, 72, 36, 0, 405, 403, 1, 0, 0, 0, 406, 409, 1, 0, 0, 0, 407, 405, 1, 0, 0, 0, 407, 408, 1, 0, 0, 0, 408, 59, 1, 0, 0, 0, 409, 407, 1, 0, 0, 0, 410, 415, 3, 66, 33, 0, 411, 412, 5, 41, 0, 0, 412, 414, 3, 66, 33, 0, 413, 411, 1, 0, 0, 0, 414, 417, 1, 0, 0, 0, 415, 413, 1, 0, 0, 0, 415, 416, 1, 0, 0, 0, 416, 61, 1, 0, 0, 0, 417, 415, 1, 0, 0, 0, 418, 423, 3, 60, 30, 0, 419, 420, 5, 39, 0, 0, 420, 422, 3, 60, 30, 0, 421, 419, 1, 0, 0, 0, 422, 425, 1, 0, 0, 0, 423, 421, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 63, 1, 0, 0, 0, 425, 423, 1, 0, 0, 0, 426, 427, 7, 3, 0, 0, 427, 65, 1, 0, 0, 0, 428, 432, 5, 85, 0, 0, 429, 430, 4, 33, 10, 0, 430, 432, 3, 70, 35, 0, 431, 428, 1, 0, 0, 0, 431, 429, 1, 0, 0, 0, 432, 67, 1, 0, 0, 0, 433, 476, 5, 50, 0, 0, 434, 435, 3, 104, 52, 0, 435, 436, 5, 72, 0, 0, 436, 476, 1, 0, 0, 0, 437, 476, 3, 102, 51, 0, 438, 476, 3, 104, 52, 0, 439, 476, 3, 98, 49, 0, 440, 476, 3, 70, 35, 0, 441, 476, 3, 106, 53, 0, 442, 443, 5, 70, 0, 0, 443, 448, 3, 100, 50, 0, 444, 445, 5, 39, 0, 0, 445, 447, 3, 100, 50, 0, 446, 444, 1, 0, 0, 0, 447, 450, 1, 0, 0, 0, 448, 446, 1, 0, 0, 0, 448, 449, 1, 0, 0, 0, 449, 451, 1, 0, 0, 0, 450, 448, 1, 0, 0, 0, 451, 452, 5, 71, 0, 0, 452, 476, 1, 0, 0, 0, 453, 454, 5, 70, 0, 0, 454, 459, 3, 98, 49, 0, 455, 456, 5, 39, 0, 0, 456, 458, 3, 98, 49, 0, 457, 455, 1, 0, 0, 0, 458, 461, 1, 0, 0, 0, 459, 457, 1, 0, 0, 0, 459, 460, 1, 0, 0, 0, 460, 462, 1, 0, 0, 0, 461, 459, 1, 0, 0, 0, 462, 463, 5, 71, 0, 0, 463, 476, 1, 0, 0, 0, 464, 465, 5, 70, 0, 0, 465, 470, 3, 106, 53, 0, 466, 467, 5, 39, 0, 0, 467, 469, 3, 106, 53, 0, 468, 466, 1, 0, 0, 0, 469, 472, 1, 0, 0, 0, 470, 468, 1, 0, 0, 0, 470, 471, 1, 0, 0, 0, 471, 473, 1, 0, 0, 0, 472, 470, 1, 0, 0, 0, 473, 474, 5, 71, 0, 0, 474, 476, 1, 0, 0, 0, 475, 433, 1, 0, 0, 0, 475, 434, 1, 0, 0, 0, 475, 437, 1, 0, 0, 0, 475, 438, 1, 0, 0, 0, 475, 439, 1, 0, 0, 0, 475, 440, 1, 0, 0, 0, 475, 441, 1, 0, 0, 0, 475, 442, 1, 0, 0, 0, 475, 453, 1, 0, 0, 0, 475, 464, 1, 0, 0, 0, 476, 69, 1, 0, 0, 0, 477, 480, 5, 53, 0, 0, 478, 480, 5, 69, 0, 0, 479, 477, 1, 0, 0, 0, 479, 478, 1, 0, 0, 0, 480, 71, 1, 0, 0, 0, 481, 485, 3, 64, 32, 0, 482, 483, 4, 36, 11, 0, 483, 485, 3, 70, 35, 0, 484, 481, 1, 0, 0, 0, 484, 482, 1, 0, 0, 0, 485, 73, 1, 0, 0, 0, 486, 487, 5, 9, 0, 0, 487, 488, 5, 31, 0, 0, 488, 75, 1, 0, 0, 0, 489, 490, 5, 14, 0, 0, 490, 495, 3, 78, 39, 0, 491, 492, 5, 39, 0, 0, 492, 494, 3, 78, 39, 0, 493, 491, 1, 0, 0, 0, 494, 497, 1, 0, 0, 0, 495, 493, 1, 0, 0, 0, 495, 496, 1, 0, 0, 0, 496, 77, 1, 0, 0, 0, 497, 495, 1, 0, 0, 0, 498, 500, 3, 10, 5, 0, 499, 501, 7, 4, 0, 0, 500, 499, 1, 0, 0, 0, 500, 501, 1, 0, 0, 0, 501, 504, 1, 0, 0, 0, 502, 503, 5, 51, 0, 0, 503, 505, 7, 5, 0, 0, 504, 502, 1, 0, 0, 0, 504, 505, 1, 0, 0, 0, 505, 79, 1, 0, 0, 0, 506, 507, 5, 8, 0, 0, 507, 508, 3, 62, 31, 0, 508, 81, 1, 0, 0, 0, 509, 510, 5, 2, 0, 0, 510, 511, 3, 62, 31, 0, 511, 83, 1, 0, 0, 0, 512, 513, 5, 11, 0, 0, 513, 518, 3, 86, 43, 0, 514, 515, 5, 39, 0, 0, 515, 517, 3, 86, 43, 0, 516, 514, 1, 0, 0, 0, 517, 520, 1, 0, 0, 0, 518, 516, 1, 0, 0, 0, 518, 519, 1, 0, 0, 0, 519, 85, 1, 0, 0, 0, 520, 518, 1, 0, 0, 0, 521, 522, 3, 60, 30, 0, 522, 523, 5, 89, 0, 0, 523, 524, 3, 60, 30, 0, 524, 87, 1, 0, 0, 0, 525, 526, 5, 1, 0, 0, 526, 527, 3, 20, 10, 0, 527, 529, 3, 106, 53, 0, 528, 530, 3, 94, 47, 0, 529, 528, 1, 0, 0, 0, 529, 530, 1, 0, 0, 0, 530, 89, 1, 0, 0, 0, 531, 532, 5, 7, 0, 0, 532, 533, 3, 20, 10, 0, 533, 534, 3, 106, 53, 0, 534, 91, 1, 0, 0, 0, 535, 536, 5, 10, 0, 0, 536, 537, 3, 58, 29, 0, 537, 93, 1, 0, 0, 0, 538, 543, 3, 96, 48, 0, 539, 540, 5, 39, 0, 0, 540, 542, 3, 96, 48, 0, 541, 539, 1, 0, 0, 0, 542, 545, 1, 0, 0, 0, 543, 541, 1, 0, 0, 0, 543, 544, 1, 0, 0, 0, 544, 95, 1, 0, 0, 0, 545, 543, 1, 0, 0, 0, 546, 547, 3, 64, 32, 0, 547, 548, 5, 36, 0, 0, 548, 549, 3, 68, 34, 0, 549, 97, 1, 0, 0, 0, 550, 551, 7, 6, 0, 0, 551, 99, 1, 0, 0, 0, 552, 555, 3, 102, 51, 0, 553, 555, 3, 104, 52, 0, 554, 552, 1, 0, 0, 0, 554, 553, 1, 0, 0, 0, 555, 101, 1, 0, 0, 0, 556, 558, 7, 0, 0, 0, 557, 556, 1, 0, 0, 0, 557, 558, 1, 0, 0, 0, 558, 559, 1, 0, 0, 0, 559, 560, 5, 32, 0, 0, 560, 103, 1, 0, 0, 0, 561, 563, 7, 0, 0, 0, 562, 561, 1, 0, 0, 0, 562, 563, 1, 0, 0, 0, 563, 564, 1, 0, 0, 0, 564, 565, 5, 31, 0, 0, 565, 105, 1, 0, 0, 0, 566, 567, 5, 30, 0, 0, 567, 107, 1, 0, 0, 0, 568, 569, 7, 7, 0, 0, 569, 109, 1, 0, 0, 0, 570, 571, 5, 5, 0, 0, 571, 572, 3, 112, 56, 0, 572, 111, 1, 0, 0, 0, 573, 574, 5, 70, 0, 0, 574, 575, 3, 2, 1, 0, 575, 576, 5, 71, 0, 0, 576, 113, 1, 0, 0, 0, 577, 578, 5, 13, 0, 0, 578, 579, 5, 105, 0, 0, 579, 115, 1, 0, 0, 0, 580, 581, 5, 3, 0, 0, 581, 584, 5, 95, 0, 0, 582, 583, 5, 93, 0, 0, 583, 585, 3, 60, 30, 0, 584, 582, 1, 0, 0, 0, 584, 585, 1, 0, 0, 0, 585, 595, 1, 0, 0, 0, 586, 587, 5, 94, 0, 0, 587, 592, 3, 118, 59, 0, 588, 589, 5, 39, 0, 0, 589, 591, 3, 118, 59, 0, 590, 588, 1, 0, 0, 0, 591, 594, 1, 0, 0, 0, 592, 590, 1, 0, 0, 0, 592, 593, 1, 0, 0, 0, 593, 596, 1, 0, 0, 0, 594, 592, 1, 0, 0, 0, 595, 586, 1, 0, 0, 0, 595, 596, 1, 0, 0, 0, 596, 117, 1, 0, 0, 0, 597, 598, 3, 60, 30, 0, 598, 599, 5, 36, 0, 0, 599, 601, 1, 0, 0, 0, 600, 597, 1, 0, 0, 0, 600, 601, 1, 0, 0, 0, 601, 602, 1, 0, 0, 0, 602, 603, 3, 60, 30, 0, 603, 119, 1, 0, 0, 0, 604, 605, 5, 18, 0, 0, 605, 606, 3, 36, 18, 0, 606, 607, 5, 93, 0, 0, 607, 608, 3, 62, 31, 0, 608, 121, 1, 0, 0, 0, 609, 610, 5, 17, 0, 0, 610, 613, 3, 54, 27, 0, 611, 612, 5, 33, 0, 0, 612, 614, 3, 30, 15, 0, 613, 611, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 123, 1, 0, 0, 0, 615, 617, 7, 8, 0, 0, 616, 615, 1, 0, 0, 0, 616, 617, 1, 0, 0, 0, 617, 618, 1, 0, 0, 0, 618, 619, 5, 20, 0, 0, 619, 620, 3, 126, 63, 0, 620, 621, 3, 128, 64, 0, 621, 125, 1, 0, 0, 0, 622, 625, 3, 64, 32, 0, 623, 624, 5, 89, 0, 0, 624, 626, 3, 64, 32, 0, 625, 623, 1, 0, 0, 0, 625, 626, 1, 0, 0, 0, 626, 127, 1, 0, 0, 0, 627, 628, 5, 93, 0, 0, 628, 633, 3, 130, 65, 0, 629, 630, 5, 39, 0, 0, 630, 632, 3, 130, 65, 0, 631, 629, 1, 0, 0, 0, 632, 635, 1, 0, 0, 0, 633, 631, 1, 0, 0, 0, 633, 634, 1, 0, 0, 0, 634, 129, 1, 0, 0, 0, 635, 633, 1, 0, 0, 0, 636, 637, 3, 16, 8, 0, 637, 131, 1, 0, 0, 0, 62, 143, 152, 172, 184, 193, 201, 206, 214, 216, 221, 228, 233, 238, 248, 254, 262, 264, 275, 282, 293, 296, 312, 318, 328, 332, 337, 347, 355, 368, 372, 376, 383, 387, 394, 400, 407, 415, 423, 431, 448, 459, 470, 475, 479, 484, 495, 500, 504, 518, 529, 543, 554, 557, 562, 584, 592, 595, 600, 613, 616, 625, 633] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java index e864eaff3edd..a56035364641 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java @@ -1173,7 +1173,8 @@ public final RegexBooleanExpressionContext regexBooleanExpression() throws Recog @SuppressWarnings("CheckReturnValue") public static class MatchBooleanExpressionContext extends ParserRuleContext { public QualifiedNameContext fieldExp; - public ConstantContext queryString; + public DataTypeContext fieldType; + public ConstantContext matchQuery; public TerminalNode COLON() { return getToken(EsqlBaseParser.COLON, 0); } public QualifiedNameContext qualifiedName() { return getRuleContext(QualifiedNameContext.class,0); @@ -1181,6 +1182,10 @@ public QualifiedNameContext qualifiedName() { public ConstantContext constant() { return getRuleContext(ConstantContext.class,0); } + public TerminalNode CAST_OP() { return getToken(EsqlBaseParser.CAST_OP, 0); } + public DataTypeContext dataType() { + return getRuleContext(DataTypeContext.class,0); + } @SuppressWarnings("this-escape") public MatchBooleanExpressionContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); @@ -1204,15 +1209,28 @@ public T accept(ParseTreeVisitor visitor) { public final MatchBooleanExpressionContext matchBooleanExpression() throws RecognitionException { MatchBooleanExpressionContext _localctx = new MatchBooleanExpressionContext(_ctx, getState()); enterRule(_localctx, 14, RULE_matchBooleanExpression); + int _la; try { enterOuterAlt(_localctx, 1); { setState(235); ((MatchBooleanExpressionContext)_localctx).fieldExp = qualifiedName(); - setState(236); + setState(238); + _errHandler.sync(this); + _la = _input.LA(1); + if (_la==CAST_OP) { + { + setState(236); + match(CAST_OP); + setState(237); + ((MatchBooleanExpressionContext)_localctx).fieldType = dataType(); + } + } + + setState(240); match(COLON); - setState(237); - ((MatchBooleanExpressionContext)_localctx).queryString = constant(); + setState(241); + ((MatchBooleanExpressionContext)_localctx).matchQuery = constant(); } } catch (RecognitionException re) { @@ -1295,14 +1313,14 @@ public final ValueExpressionContext valueExpression() throws RecognitionExceptio ValueExpressionContext _localctx = new ValueExpressionContext(_ctx, getState()); enterRule(_localctx, 16, RULE_valueExpression); try { - setState(244); + setState(248); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,12,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,13,_ctx) ) { case 1: _localctx = new ValueExpressionDefaultContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(239); + setState(243); operatorExpression(0); } break; @@ -1310,11 +1328,11 @@ public final ValueExpressionContext valueExpression() throws RecognitionExceptio _localctx = new ComparisonContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(240); + setState(244); ((ComparisonContext)_localctx).left = operatorExpression(0); - setState(241); + setState(245); comparisonOperator(); - setState(242); + setState(246); ((ComparisonContext)_localctx).right = operatorExpression(0); } break; @@ -1439,16 +1457,16 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE int _alt; enterOuterAlt(_localctx, 1); { - setState(250); + setState(254); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,13,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,14,_ctx) ) { case 1: { _localctx = new OperatorExpressionDefaultContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(247); + setState(251); primaryExpression(0); } break; @@ -1457,7 +1475,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _localctx = new ArithmeticUnaryContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(248); + setState(252); ((ArithmeticUnaryContext)_localctx).operator = _input.LT(1); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -1468,31 +1486,31 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _errHandler.reportMatch(this); consume(); } - setState(249); + setState(253); operatorExpression(3); } break; } _ctx.stop = _input.LT(-1); - setState(260); + setState(264); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,15,_ctx); + _alt = getInterpreter().adaptivePredict(_input,16,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( _parseListeners!=null ) triggerExitRuleEvent(); _prevctx = _localctx; { - setState(258); + setState(262); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,14,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,15,_ctx) ) { case 1: { _localctx = new ArithmeticBinaryContext(new OperatorExpressionContext(_parentctx, _parentState)); ((ArithmeticBinaryContext)_localctx).left = _prevctx; pushNewRecursionContext(_localctx, _startState, RULE_operatorExpression); - setState(252); + setState(256); if (!(precpred(_ctx, 2))) throw new FailedPredicateException(this, "precpred(_ctx, 2)"); - setState(253); + setState(257); ((ArithmeticBinaryContext)_localctx).operator = _input.LT(1); _la = _input.LA(1); if ( !(((((_la - 66)) & ~0x3f) == 0 && ((1L << (_la - 66)) & 7L) != 0)) ) { @@ -1503,7 +1521,7 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _errHandler.reportMatch(this); consume(); } - setState(254); + setState(258); ((ArithmeticBinaryContext)_localctx).right = operatorExpression(3); } break; @@ -1512,9 +1530,9 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _localctx = new ArithmeticBinaryContext(new OperatorExpressionContext(_parentctx, _parentState)); ((ArithmeticBinaryContext)_localctx).left = _prevctx; pushNewRecursionContext(_localctx, _startState, RULE_operatorExpression); - setState(255); + setState(259); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(256); + setState(260); ((ArithmeticBinaryContext)_localctx).operator = _input.LT(1); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -1525,16 +1543,16 @@ private OperatorExpressionContext operatorExpression(int _p) throws RecognitionE _errHandler.reportMatch(this); consume(); } - setState(257); + setState(261); ((ArithmeticBinaryContext)_localctx).right = operatorExpression(2); } break; } } } - setState(262); + setState(266); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,15,_ctx); + _alt = getInterpreter().adaptivePredict(_input,16,_ctx); } } } @@ -1690,16 +1708,16 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc int _alt; enterOuterAlt(_localctx, 1); { - setState(271); + setState(275); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,16,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,17,_ctx) ) { case 1: { _localctx = new ConstantDefaultContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(264); + setState(268); constant(); } break; @@ -1708,7 +1726,7 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _localctx = new DereferenceContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(265); + setState(269); qualifiedName(); } break; @@ -1717,7 +1735,7 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _localctx = new FunctionContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(266); + setState(270); functionExpression(); } break; @@ -1726,19 +1744,19 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc _localctx = new ParenthesizedExpressionContext(_localctx); _ctx = _localctx; _prevctx = _localctx; - setState(267); + setState(271); match(LP); - setState(268); + setState(272); booleanExpression(0); - setState(269); + setState(273); match(RP); } break; } _ctx.stop = _input.LT(-1); - setState(278); + setState(282); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,17,_ctx); + _alt = getInterpreter().adaptivePredict(_input,18,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( _parseListeners!=null ) triggerExitRuleEvent(); @@ -1747,18 +1765,18 @@ private PrimaryExpressionContext primaryExpression(int _p) throws RecognitionExc { _localctx = new InlineCastContext(new PrimaryExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_primaryExpression); - setState(273); + setState(277); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(274); + setState(278); match(CAST_OP); - setState(275); + setState(279); dataType(); } } } - setState(280); + setState(284); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,17,_ctx); + _alt = getInterpreter().adaptivePredict(_input,18,_ctx); } } } @@ -1818,37 +1836,37 @@ public final FunctionExpressionContext functionExpression() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(281); + setState(285); functionName(); - setState(282); + setState(286); match(LP); - setState(292); + setState(296); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,19,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,20,_ctx) ) { case 1: { - setState(283); + setState(287); match(ASTERISK); } break; case 2: { { - setState(284); + setState(288); booleanExpression(0); - setState(289); + setState(293); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(285); + setState(289); match(COMMA); - setState(286); + setState(290); booleanExpression(0); } } - setState(291); + setState(295); _errHandler.sync(this); _la = _input.LA(1); } @@ -1856,7 +1874,7 @@ public final FunctionExpressionContext functionExpression() throws RecognitionEx } break; } - setState(294); + setState(298); match(RP); } } @@ -1902,7 +1920,7 @@ public final FunctionNameContext functionName() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(296); + setState(300); identifierOrParameter(); } } @@ -1960,7 +1978,7 @@ public final DataTypeContext dataType() throws RecognitionException { _localctx = new ToDataTypeContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(298); + setState(302); identifier(); } } @@ -2007,9 +2025,9 @@ public final RowCommandContext rowCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(300); + setState(304); match(ROW); - setState(301); + setState(305); fields(); } } @@ -2063,25 +2081,25 @@ public final FieldsContext fields() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(303); + setState(307); field(); - setState(308); + setState(312); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,20,_ctx); + _alt = getInterpreter().adaptivePredict(_input,21,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(304); + setState(308); match(COMMA); - setState(305); + setState(309); field(); } } } - setState(310); + setState(314); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,20,_ctx); + _alt = getInterpreter().adaptivePredict(_input,21,_ctx); } } } @@ -2131,19 +2149,19 @@ public final FieldContext field() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(314); + setState(318); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,21,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,22,_ctx) ) { case 1: { - setState(311); + setState(315); qualifiedName(); - setState(312); + setState(316); match(ASSIGN); } break; } - setState(316); + setState(320); booleanExpression(0); } } @@ -2201,34 +2219,34 @@ public final FromCommandContext fromCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(318); + setState(322); match(FROM); - setState(319); + setState(323); indexPattern(); - setState(324); + setState(328); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,22,_ctx); + _alt = getInterpreter().adaptivePredict(_input,23,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(320); + setState(324); match(COMMA); - setState(321); + setState(325); indexPattern(); } } } - setState(326); + setState(330); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,22,_ctx); + _alt = getInterpreter().adaptivePredict(_input,23,_ctx); } - setState(328); + setState(332); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,23,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,24,_ctx) ) { case 1: { - setState(327); + setState(331); metadata(); } break; @@ -2281,19 +2299,19 @@ public final IndexPatternContext indexPattern() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(333); + setState(337); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,24,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,25,_ctx) ) { case 1: { - setState(330); + setState(334); clusterString(); - setState(331); + setState(335); match(COLON); } break; } - setState(335); + setState(339); indexString(); } } @@ -2337,7 +2355,7 @@ public final ClusterStringContext clusterString() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(337); + setState(341); match(UNQUOTED_SOURCE); } } @@ -2383,7 +2401,7 @@ public final IndexStringContext indexString() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(339); + setState(343); _la = _input.LA(1); if ( !(_la==QUOTED_STRING || _la==UNQUOTED_SOURCE) ) { _errHandler.recoverInline(this); @@ -2438,20 +2456,20 @@ public final MetadataContext metadata() throws RecognitionException { MetadataContext _localctx = new MetadataContext(_ctx, getState()); enterRule(_localctx, 42, RULE_metadata); try { - setState(343); + setState(347); _errHandler.sync(this); switch (_input.LA(1)) { case METADATA: enterOuterAlt(_localctx, 1); { - setState(341); + setState(345); metadataOption(); } break; case OPENING_BRACKET: enterOuterAlt(_localctx, 2); { - setState(342); + setState(346); deprecated_metadata(); } break; @@ -2508,27 +2526,27 @@ public final MetadataOptionContext metadataOption() throws RecognitionException int _alt; enterOuterAlt(_localctx, 1); { - setState(345); + setState(349); match(METADATA); - setState(346); + setState(350); match(UNQUOTED_SOURCE); - setState(351); + setState(355); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,26,_ctx); + _alt = getInterpreter().adaptivePredict(_input,27,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(347); + setState(351); match(COMMA); - setState(348); + setState(352); match(UNQUOTED_SOURCE); } } } - setState(353); + setState(357); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,26,_ctx); + _alt = getInterpreter().adaptivePredict(_input,27,_ctx); } } } @@ -2575,11 +2593,11 @@ public final Deprecated_metadataContext deprecated_metadata() throws Recognition try { enterOuterAlt(_localctx, 1); { - setState(354); + setState(358); match(OPENING_BRACKET); - setState(355); + setState(359); metadataOption(); - setState(356); + setState(360); match(CLOSING_BRACKET); } } @@ -2643,46 +2661,46 @@ public final MetricsCommandContext metricsCommand() throws RecognitionException int _alt; enterOuterAlt(_localctx, 1); { - setState(358); + setState(362); match(DEV_METRICS); - setState(359); + setState(363); indexPattern(); - setState(364); + setState(368); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,27,_ctx); + _alt = getInterpreter().adaptivePredict(_input,28,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(360); + setState(364); match(COMMA); - setState(361); + setState(365); indexPattern(); } } } - setState(366); + setState(370); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,27,_ctx); + _alt = getInterpreter().adaptivePredict(_input,28,_ctx); } - setState(368); + setState(372); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,28,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,29,_ctx) ) { case 1: { - setState(367); + setState(371); ((MetricsCommandContext)_localctx).aggregates = aggFields(); } break; } - setState(372); + setState(376); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,29,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,30,_ctx) ) { case 1: { - setState(370); + setState(374); match(BY); - setState(371); + setState(375); ((MetricsCommandContext)_localctx).grouping = fields(); } break; @@ -2732,9 +2750,9 @@ public final EvalCommandContext evalCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(374); + setState(378); match(EVAL); - setState(375); + setState(379); fields(); } } @@ -2787,26 +2805,26 @@ public final StatsCommandContext statsCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(377); + setState(381); match(STATS); - setState(379); + setState(383); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,30,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,31,_ctx) ) { case 1: { - setState(378); + setState(382); ((StatsCommandContext)_localctx).stats = aggFields(); } break; } - setState(383); + setState(387); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,31,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,32,_ctx) ) { case 1: { - setState(381); + setState(385); match(BY); - setState(382); + setState(386); ((StatsCommandContext)_localctx).grouping = fields(); } break; @@ -2863,25 +2881,25 @@ public final AggFieldsContext aggFields() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(385); + setState(389); aggField(); - setState(390); + setState(394); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,32,_ctx); + _alt = getInterpreter().adaptivePredict(_input,33,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(386); + setState(390); match(COMMA); - setState(387); + setState(391); aggField(); } } } - setState(392); + setState(396); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,32,_ctx); + _alt = getInterpreter().adaptivePredict(_input,33,_ctx); } } } @@ -2931,16 +2949,16 @@ public final AggFieldContext aggField() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(393); + setState(397); field(); - setState(396); + setState(400); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,33,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,34,_ctx) ) { case 1: { - setState(394); + setState(398); match(WHERE); - setState(395); + setState(399); booleanExpression(0); } break; @@ -2997,25 +3015,25 @@ public final QualifiedNameContext qualifiedName() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(398); + setState(402); identifierOrParameter(); - setState(403); + setState(407); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,34,_ctx); + _alt = getInterpreter().adaptivePredict(_input,35,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(399); + setState(403); match(DOT); - setState(400); + setState(404); identifierOrParameter(); } } } - setState(405); + setState(409); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,34,_ctx); + _alt = getInterpreter().adaptivePredict(_input,35,_ctx); } } } @@ -3069,25 +3087,25 @@ public final QualifiedNamePatternContext qualifiedNamePattern() throws Recogniti int _alt; enterOuterAlt(_localctx, 1); { - setState(406); + setState(410); identifierPattern(); - setState(411); + setState(415); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,35,_ctx); + _alt = getInterpreter().adaptivePredict(_input,36,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(407); + setState(411); match(DOT); - setState(408); + setState(412); identifierPattern(); } } } - setState(413); + setState(417); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,35,_ctx); + _alt = getInterpreter().adaptivePredict(_input,36,_ctx); } } } @@ -3141,25 +3159,25 @@ public final QualifiedNamePatternsContext qualifiedNamePatterns() throws Recogni int _alt; enterOuterAlt(_localctx, 1); { - setState(414); + setState(418); qualifiedNamePattern(); - setState(419); + setState(423); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,36,_ctx); + _alt = getInterpreter().adaptivePredict(_input,37,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(415); + setState(419); match(COMMA); - setState(416); + setState(420); qualifiedNamePattern(); } } } - setState(421); + setState(425); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,36,_ctx); + _alt = getInterpreter().adaptivePredict(_input,37,_ctx); } } } @@ -3205,7 +3223,7 @@ public final IdentifierContext identifier() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(422); + setState(426); _la = _input.LA(1); if ( !(_la==UNQUOTED_IDENTIFIER || _la==QUOTED_IDENTIFIER) ) { _errHandler.recoverInline(this); @@ -3258,22 +3276,22 @@ public final IdentifierPatternContext identifierPattern() throws RecognitionExce IdentifierPatternContext _localctx = new IdentifierPatternContext(_ctx, getState()); enterRule(_localctx, 66, RULE_identifierPattern); try { - setState(427); + setState(431); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,37,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,38,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(424); + setState(428); match(ID_PATTERN); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(425); + setState(429); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(426); + setState(430); parameter(); } break; @@ -3546,14 +3564,14 @@ public final ConstantContext constant() throws RecognitionException { enterRule(_localctx, 68, RULE_constant); int _la; try { - setState(471); + setState(475); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,41,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,42,_ctx) ) { case 1: _localctx = new NullLiteralContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(429); + setState(433); match(NULL); } break; @@ -3561,9 +3579,9 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new QualifiedIntegerLiteralContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(430); + setState(434); integerValue(); - setState(431); + setState(435); match(UNQUOTED_IDENTIFIER); } break; @@ -3571,7 +3589,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new DecimalLiteralContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(433); + setState(437); decimalValue(); } break; @@ -3579,7 +3597,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new IntegerLiteralContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(434); + setState(438); integerValue(); } break; @@ -3587,7 +3605,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new BooleanLiteralContext(_localctx); enterOuterAlt(_localctx, 5); { - setState(435); + setState(439); booleanValue(); } break; @@ -3595,7 +3613,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new InputParameterContext(_localctx); enterOuterAlt(_localctx, 6); { - setState(436); + setState(440); parameter(); } break; @@ -3603,7 +3621,7 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new StringLiteralContext(_localctx); enterOuterAlt(_localctx, 7); { - setState(437); + setState(441); string(); } break; @@ -3611,27 +3629,27 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new NumericArrayLiteralContext(_localctx); enterOuterAlt(_localctx, 8); { - setState(438); + setState(442); match(OPENING_BRACKET); - setState(439); + setState(443); numericValue(); - setState(444); + setState(448); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(440); + setState(444); match(COMMA); - setState(441); + setState(445); numericValue(); } } - setState(446); + setState(450); _errHandler.sync(this); _la = _input.LA(1); } - setState(447); + setState(451); match(CLOSING_BRACKET); } break; @@ -3639,27 +3657,27 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new BooleanArrayLiteralContext(_localctx); enterOuterAlt(_localctx, 9); { - setState(449); + setState(453); match(OPENING_BRACKET); - setState(450); + setState(454); booleanValue(); - setState(455); + setState(459); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(451); + setState(455); match(COMMA); - setState(452); + setState(456); booleanValue(); } } - setState(457); + setState(461); _errHandler.sync(this); _la = _input.LA(1); } - setState(458); + setState(462); match(CLOSING_BRACKET); } break; @@ -3667,27 +3685,27 @@ public final ConstantContext constant() throws RecognitionException { _localctx = new StringArrayLiteralContext(_localctx); enterOuterAlt(_localctx, 10); { - setState(460); + setState(464); match(OPENING_BRACKET); - setState(461); + setState(465); string(); - setState(466); + setState(470); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(462); + setState(466); match(COMMA); - setState(463); + setState(467); string(); } } - setState(468); + setState(472); _errHandler.sync(this); _la = _input.LA(1); } - setState(469); + setState(473); match(CLOSING_BRACKET); } break; @@ -3761,14 +3779,14 @@ public final ParameterContext parameter() throws RecognitionException { ParameterContext _localctx = new ParameterContext(_ctx, getState()); enterRule(_localctx, 70, RULE_parameter); try { - setState(475); + setState(479); _errHandler.sync(this); switch (_input.LA(1)) { case PARAM: _localctx = new InputParamContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(473); + setState(477); match(PARAM); } break; @@ -3776,7 +3794,7 @@ public final ParameterContext parameter() throws RecognitionException { _localctx = new InputNamedOrPositionalParamContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(474); + setState(478); match(NAMED_OR_POSITIONAL_PARAM); } break; @@ -3827,22 +3845,22 @@ public final IdentifierOrParameterContext identifierOrParameter() throws Recogni IdentifierOrParameterContext _localctx = new IdentifierOrParameterContext(_ctx, getState()); enterRule(_localctx, 72, RULE_identifierOrParameter); try { - setState(480); + setState(484); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,43,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,44,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(477); + setState(481); identifier(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(478); + setState(482); if (!(this.isDevVersion())) throw new FailedPredicateException(this, "this.isDevVersion()"); - setState(479); + setState(483); parameter(); } break; @@ -3889,9 +3907,9 @@ public final LimitCommandContext limitCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(482); + setState(486); match(LIMIT); - setState(483); + setState(487); match(INTEGER_LITERAL); } } @@ -3946,27 +3964,27 @@ public final SortCommandContext sortCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(485); + setState(489); match(SORT); - setState(486); + setState(490); orderExpression(); - setState(491); + setState(495); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,44,_ctx); + _alt = getInterpreter().adaptivePredict(_input,45,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(487); + setState(491); match(COMMA); - setState(488); + setState(492); orderExpression(); } } } - setState(493); + setState(497); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,44,_ctx); + _alt = getInterpreter().adaptivePredict(_input,45,_ctx); } } } @@ -4020,14 +4038,14 @@ public final OrderExpressionContext orderExpression() throws RecognitionExceptio try { enterOuterAlt(_localctx, 1); { - setState(494); + setState(498); booleanExpression(0); - setState(496); + setState(500); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,45,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,46,_ctx) ) { case 1: { - setState(495); + setState(499); ((OrderExpressionContext)_localctx).ordering = _input.LT(1); _la = _input.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -4041,14 +4059,14 @@ public final OrderExpressionContext orderExpression() throws RecognitionExceptio } break; } - setState(500); + setState(504); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,46,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,47,_ctx) ) { case 1: { - setState(498); + setState(502); match(NULLS); - setState(499); + setState(503); ((OrderExpressionContext)_localctx).nullOrdering = _input.LT(1); _la = _input.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -4107,9 +4125,9 @@ public final KeepCommandContext keepCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(502); + setState(506); match(KEEP); - setState(503); + setState(507); qualifiedNamePatterns(); } } @@ -4156,9 +4174,9 @@ public final DropCommandContext dropCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(505); + setState(509); match(DROP); - setState(506); + setState(510); qualifiedNamePatterns(); } } @@ -4213,27 +4231,27 @@ public final RenameCommandContext renameCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(508); + setState(512); match(RENAME); - setState(509); + setState(513); renameClause(); - setState(514); + setState(518); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,47,_ctx); + _alt = getInterpreter().adaptivePredict(_input,48,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(510); + setState(514); match(COMMA); - setState(511); + setState(515); renameClause(); } } } - setState(516); + setState(520); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,47,_ctx); + _alt = getInterpreter().adaptivePredict(_input,48,_ctx); } } } @@ -4285,11 +4303,11 @@ public final RenameClauseContext renameClause() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(517); + setState(521); ((RenameClauseContext)_localctx).oldName = qualifiedNamePattern(); - setState(518); + setState(522); match(AS); - setState(519); + setState(523); ((RenameClauseContext)_localctx).newName = qualifiedNamePattern(); } } @@ -4342,18 +4360,18 @@ public final DissectCommandContext dissectCommand() throws RecognitionException try { enterOuterAlt(_localctx, 1); { - setState(521); + setState(525); match(DISSECT); - setState(522); + setState(526); primaryExpression(0); - setState(523); + setState(527); string(); - setState(525); + setState(529); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,48,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,49,_ctx) ) { case 1: { - setState(524); + setState(528); commandOptions(); } break; @@ -4406,11 +4424,11 @@ public final GrokCommandContext grokCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(527); + setState(531); match(GROK); - setState(528); + setState(532); primaryExpression(0); - setState(529); + setState(533); string(); } } @@ -4457,9 +4475,9 @@ public final MvExpandCommandContext mvExpandCommand() throws RecognitionExceptio try { enterOuterAlt(_localctx, 1); { - setState(531); + setState(535); match(MV_EXPAND); - setState(532); + setState(536); qualifiedName(); } } @@ -4513,25 +4531,25 @@ public final CommandOptionsContext commandOptions() throws RecognitionException int _alt; enterOuterAlt(_localctx, 1); { - setState(534); + setState(538); commandOption(); - setState(539); + setState(543); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,49,_ctx); + _alt = getInterpreter().adaptivePredict(_input,50,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(535); + setState(539); match(COMMA); - setState(536); + setState(540); commandOption(); } } } - setState(541); + setState(545); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,49,_ctx); + _alt = getInterpreter().adaptivePredict(_input,50,_ctx); } } } @@ -4581,11 +4599,11 @@ public final CommandOptionContext commandOption() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(542); + setState(546); identifier(); - setState(543); + setState(547); match(ASSIGN); - setState(544); + setState(548); constant(); } } @@ -4631,7 +4649,7 @@ public final BooleanValueContext booleanValue() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(546); + setState(550); _la = _input.LA(1); if ( !(_la==FALSE || _la==TRUE) ) { _errHandler.recoverInline(this); @@ -4686,20 +4704,20 @@ public final NumericValueContext numericValue() throws RecognitionException { NumericValueContext _localctx = new NumericValueContext(_ctx, getState()); enterRule(_localctx, 100, RULE_numericValue); try { - setState(550); + setState(554); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,50,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,51,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(548); + setState(552); decimalValue(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(549); + setState(553); integerValue(); } break; @@ -4748,12 +4766,12 @@ public final DecimalValueContext decimalValue() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(553); + setState(557); _errHandler.sync(this); _la = _input.LA(1); if (_la==PLUS || _la==MINUS) { { - setState(552); + setState(556); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { _errHandler.recoverInline(this); @@ -4766,7 +4784,7 @@ public final DecimalValueContext decimalValue() throws RecognitionException { } } - setState(555); + setState(559); match(DECIMAL_LITERAL); } } @@ -4813,12 +4831,12 @@ public final IntegerValueContext integerValue() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(558); + setState(562); _errHandler.sync(this); _la = _input.LA(1); if (_la==PLUS || _la==MINUS) { { - setState(557); + setState(561); _la = _input.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { _errHandler.recoverInline(this); @@ -4831,7 +4849,7 @@ public final IntegerValueContext integerValue() throws RecognitionException { } } - setState(560); + setState(564); match(INTEGER_LITERAL); } } @@ -4875,7 +4893,7 @@ public final StringContext string() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(562); + setState(566); match(QUOTED_STRING); } } @@ -4925,7 +4943,7 @@ public final ComparisonOperatorContext comparisonOperator() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(564); + setState(568); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & -432345564227567616L) != 0)) ) { _errHandler.recoverInline(this); @@ -4980,9 +4998,9 @@ public final ExplainCommandContext explainCommand() throws RecognitionException try { enterOuterAlt(_localctx, 1); { - setState(566); + setState(570); match(EXPLAIN); - setState(567); + setState(571); subqueryExpression(); } } @@ -5030,11 +5048,11 @@ public final SubqueryExpressionContext subqueryExpression() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(569); + setState(573); match(OPENING_BRACKET); - setState(570); + setState(574); query(0); - setState(571); + setState(575); match(CLOSING_BRACKET); } } @@ -5091,9 +5109,9 @@ public final ShowCommandContext showCommand() throws RecognitionException { _localctx = new ShowInfoContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(573); + setState(577); match(SHOW); - setState(574); + setState(578); match(INFO); } } @@ -5156,48 +5174,48 @@ public final EnrichCommandContext enrichCommand() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(576); + setState(580); match(ENRICH); - setState(577); + setState(581); ((EnrichCommandContext)_localctx).policyName = match(ENRICH_POLICY_NAME); - setState(580); + setState(584); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,53,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,54,_ctx) ) { case 1: { - setState(578); + setState(582); match(ON); - setState(579); + setState(583); ((EnrichCommandContext)_localctx).matchField = qualifiedNamePattern(); } break; } - setState(591); + setState(595); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,55,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,56,_ctx) ) { case 1: { - setState(582); + setState(586); match(WITH); - setState(583); + setState(587); enrichWithClause(); - setState(588); + setState(592); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,54,_ctx); + _alt = getInterpreter().adaptivePredict(_input,55,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(584); + setState(588); match(COMMA); - setState(585); + setState(589); enrichWithClause(); } } } - setState(590); + setState(594); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,54,_ctx); + _alt = getInterpreter().adaptivePredict(_input,55,_ctx); } } break; @@ -5252,19 +5270,19 @@ public final EnrichWithClauseContext enrichWithClause() throws RecognitionExcept try { enterOuterAlt(_localctx, 1); { - setState(596); + setState(600); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,56,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,57,_ctx) ) { case 1: { - setState(593); + setState(597); ((EnrichWithClauseContext)_localctx).newName = qualifiedNamePattern(); - setState(594); + setState(598); match(ASSIGN); } break; } - setState(598); + setState(602); ((EnrichWithClauseContext)_localctx).enrichField = qualifiedNamePattern(); } } @@ -5317,13 +5335,13 @@ public final LookupCommandContext lookupCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(600); + setState(604); match(DEV_LOOKUP); - setState(601); + setState(605); ((LookupCommandContext)_localctx).tableName = indexPattern(); - setState(602); + setState(606); match(ON); - setState(603); + setState(607); ((LookupCommandContext)_localctx).matchFields = qualifiedNamePatterns(); } } @@ -5376,18 +5394,18 @@ public final InlinestatsCommandContext inlinestatsCommand() throws RecognitionEx try { enterOuterAlt(_localctx, 1); { - setState(605); + setState(609); match(DEV_INLINESTATS); - setState(606); + setState(610); ((InlinestatsCommandContext)_localctx).stats = aggFields(); - setState(609); + setState(613); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,57,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,58,_ctx) ) { case 1: { - setState(607); + setState(611); match(BY); - setState(608); + setState(612); ((InlinestatsCommandContext)_localctx).grouping = fields(); } break; @@ -5445,12 +5463,12 @@ public final JoinCommandContext joinCommand() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(612); + setState(616); _errHandler.sync(this); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 29360128L) != 0)) { { - setState(611); + setState(615); ((JoinCommandContext)_localctx).type = _input.LT(1); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 29360128L) != 0)) ) { @@ -5464,11 +5482,11 @@ public final JoinCommandContext joinCommand() throws RecognitionException { } } - setState(614); + setState(618); match(DEV_JOIN); - setState(615); + setState(619); joinTarget(); - setState(616); + setState(620); joinCondition(); } } @@ -5521,16 +5539,16 @@ public final JoinTargetContext joinTarget() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(618); + setState(622); ((JoinTargetContext)_localctx).index = identifier(); - setState(621); + setState(625); _errHandler.sync(this); _la = _input.LA(1); if (_la==AS) { { - setState(619); + setState(623); match(AS); - setState(620); + setState(624); ((JoinTargetContext)_localctx).alias = identifier(); } } @@ -5588,27 +5606,27 @@ public final JoinConditionContext joinCondition() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(623); + setState(627); match(ON); - setState(624); + setState(628); joinPredicate(); - setState(629); + setState(633); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,60,_ctx); + _alt = getInterpreter().adaptivePredict(_input,61,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(625); + setState(629); match(COMMA); - setState(626); + setState(630); joinPredicate(); } } } - setState(631); + setState(635); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,60,_ctx); + _alt = getInterpreter().adaptivePredict(_input,61,_ctx); } } } @@ -5654,7 +5672,7 @@ public final JoinPredicateContext joinPredicate() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(632); + setState(636); valueExpression(); } } @@ -5756,7 +5774,7 @@ private boolean identifierOrParameter_sempred(IdentifierOrParameterContext _loca } public static final String _serializedATN = - "\u0004\u0001\u0080\u027b\u0002\u0000\u0007\u0000\u0002\u0001\u0007\u0001"+ + "\u0004\u0001\u0080\u027f\u0002\u0000\u0007\u0000\u0002\u0001\u0007\u0001"+ "\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004\u0007\u0004"+ "\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007\u0007\u0007"+ "\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b\u0007\u000b"+ @@ -5790,372 +5808,374 @@ private boolean identifierOrParameter_sempred(IdentifierOrParameterContext _loca "\b\u0005\n\u0005\f\u0005\u00da\t\u0005\u0001\u0006\u0001\u0006\u0003\u0006"+ "\u00de\b\u0006\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0006"+ "\u0003\u0006\u00e5\b\u0006\u0001\u0006\u0001\u0006\u0001\u0006\u0003\u0006"+ - "\u00ea\b\u0006\u0001\u0007\u0001\u0007\u0001\u0007\u0001\u0007\u0001\b"+ - "\u0001\b\u0001\b\u0001\b\u0001\b\u0003\b\u00f5\b\b\u0001\t\u0001\t\u0001"+ - "\t\u0001\t\u0003\t\u00fb\b\t\u0001\t\u0001\t\u0001\t\u0001\t\u0001\t\u0001"+ - "\t\u0005\t\u0103\b\t\n\t\f\t\u0106\t\t\u0001\n\u0001\n\u0001\n\u0001\n"+ - "\u0001\n\u0001\n\u0001\n\u0001\n\u0003\n\u0110\b\n\u0001\n\u0001\n\u0001"+ - "\n\u0005\n\u0115\b\n\n\n\f\n\u0118\t\n\u0001\u000b\u0001\u000b\u0001\u000b"+ - "\u0001\u000b\u0001\u000b\u0001\u000b\u0005\u000b\u0120\b\u000b\n\u000b"+ - "\f\u000b\u0123\t\u000b\u0003\u000b\u0125\b\u000b\u0001\u000b\u0001\u000b"+ - "\u0001\f\u0001\f\u0001\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000e\u0001"+ - "\u000f\u0001\u000f\u0001\u000f\u0005\u000f\u0133\b\u000f\n\u000f\f\u000f"+ - "\u0136\t\u000f\u0001\u0010\u0001\u0010\u0001\u0010\u0003\u0010\u013b\b"+ - "\u0010\u0001\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0001\u0011\u0001"+ - "\u0011\u0005\u0011\u0143\b\u0011\n\u0011\f\u0011\u0146\t\u0011\u0001\u0011"+ - "\u0003\u0011\u0149\b\u0011\u0001\u0012\u0001\u0012\u0001\u0012\u0003\u0012"+ - "\u014e\b\u0012\u0001\u0012\u0001\u0012\u0001\u0013\u0001\u0013\u0001\u0014"+ - "\u0001\u0014\u0001\u0015\u0001\u0015\u0003\u0015\u0158\b\u0015\u0001\u0016"+ - "\u0001\u0016\u0001\u0016\u0001\u0016\u0005\u0016\u015e\b\u0016\n\u0016"+ - "\f\u0016\u0161\t\u0016\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017"+ - "\u0001\u0018\u0001\u0018\u0001\u0018\u0001\u0018\u0005\u0018\u016b\b\u0018"+ - "\n\u0018\f\u0018\u016e\t\u0018\u0001\u0018\u0003\u0018\u0171\b\u0018\u0001"+ - "\u0018\u0001\u0018\u0003\u0018\u0175\b\u0018\u0001\u0019\u0001\u0019\u0001"+ - "\u0019\u0001\u001a\u0001\u001a\u0003\u001a\u017c\b\u001a\u0001\u001a\u0001"+ - "\u001a\u0003\u001a\u0180\b\u001a\u0001\u001b\u0001\u001b\u0001\u001b\u0005"+ - "\u001b\u0185\b\u001b\n\u001b\f\u001b\u0188\t\u001b\u0001\u001c\u0001\u001c"+ - "\u0001\u001c\u0003\u001c\u018d\b\u001c\u0001\u001d\u0001\u001d\u0001\u001d"+ - "\u0005\u001d\u0192\b\u001d\n\u001d\f\u001d\u0195\t\u001d\u0001\u001e\u0001"+ - "\u001e\u0001\u001e\u0005\u001e\u019a\b\u001e\n\u001e\f\u001e\u019d\t\u001e"+ - "\u0001\u001f\u0001\u001f\u0001\u001f\u0005\u001f\u01a2\b\u001f\n\u001f"+ - "\f\u001f\u01a5\t\u001f\u0001 \u0001 \u0001!\u0001!\u0001!\u0003!\u01ac"+ - "\b!\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001"+ - "\"\u0001\"\u0001\"\u0001\"\u0001\"\u0005\"\u01bb\b\"\n\"\f\"\u01be\t\""+ - "\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0005\"\u01c6\b\"\n\""+ - "\f\"\u01c9\t\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0005\""+ - "\u01d1\b\"\n\"\f\"\u01d4\t\"\u0001\"\u0001\"\u0003\"\u01d8\b\"\u0001#"+ - "\u0001#\u0003#\u01dc\b#\u0001$\u0001$\u0001$\u0003$\u01e1\b$\u0001%\u0001"+ - "%\u0001%\u0001&\u0001&\u0001&\u0001&\u0005&\u01ea\b&\n&\f&\u01ed\t&\u0001"+ - "\'\u0001\'\u0003\'\u01f1\b\'\u0001\'\u0001\'\u0003\'\u01f5\b\'\u0001("+ - "\u0001(\u0001(\u0001)\u0001)\u0001)\u0001*\u0001*\u0001*\u0001*\u0005"+ - "*\u0201\b*\n*\f*\u0204\t*\u0001+\u0001+\u0001+\u0001+\u0001,\u0001,\u0001"+ - ",\u0001,\u0003,\u020e\b,\u0001-\u0001-\u0001-\u0001-\u0001.\u0001.\u0001"+ - ".\u0001/\u0001/\u0001/\u0005/\u021a\b/\n/\f/\u021d\t/\u00010\u00010\u0001"+ - "0\u00010\u00011\u00011\u00012\u00012\u00032\u0227\b2\u00013\u00033\u022a"+ - "\b3\u00013\u00013\u00014\u00034\u022f\b4\u00014\u00014\u00015\u00015\u0001"+ - "6\u00016\u00017\u00017\u00017\u00018\u00018\u00018\u00018\u00019\u0001"+ - "9\u00019\u0001:\u0001:\u0001:\u0001:\u0003:\u0245\b:\u0001:\u0001:\u0001"+ - ":\u0001:\u0005:\u024b\b:\n:\f:\u024e\t:\u0003:\u0250\b:\u0001;\u0001;"+ - "\u0001;\u0003;\u0255\b;\u0001;\u0001;\u0001<\u0001<\u0001<\u0001<\u0001"+ - "<\u0001=\u0001=\u0001=\u0001=\u0003=\u0262\b=\u0001>\u0003>\u0265\b>\u0001"+ - ">\u0001>\u0001>\u0001>\u0001?\u0001?\u0001?\u0003?\u026e\b?\u0001@\u0001"+ - "@\u0001@\u0001@\u0005@\u0274\b@\n@\f@\u0277\t@\u0001A\u0001A\u0001A\u0000"+ - "\u0004\u0002\n\u0012\u0014B\u0000\u0002\u0004\u0006\b\n\f\u000e\u0010"+ - "\u0012\u0014\u0016\u0018\u001a\u001c\u001e \"$&(*,.02468:<>@BDFHJLNPR"+ - "TVXZ\\^`bdfhjlnprtvxz|~\u0080\u0082\u0000\t\u0001\u0000@A\u0001\u0000"+ - "BD\u0002\u0000\u001e\u001eQQ\u0001\u0000HI\u0002\u0000##((\u0002\u0000"+ - "++..\u0002\u0000**88\u0002\u000099;?\u0001\u0000\u0016\u0018\u0294\u0000"+ - "\u0084\u0001\u0000\u0000\u0000\u0002\u0087\u0001\u0000\u0000\u0000\u0004"+ - "\u0098\u0001\u0000\u0000\u0000\u0006\u00ac\u0001\u0000\u0000\u0000\b\u00ae"+ - "\u0001\u0000\u0000\u0000\n\u00ce\u0001\u0000\u0000\u0000\f\u00e9\u0001"+ - "\u0000\u0000\u0000\u000e\u00eb\u0001\u0000\u0000\u0000\u0010\u00f4\u0001"+ - "\u0000\u0000\u0000\u0012\u00fa\u0001\u0000\u0000\u0000\u0014\u010f\u0001"+ - "\u0000\u0000\u0000\u0016\u0119\u0001\u0000\u0000\u0000\u0018\u0128\u0001"+ - "\u0000\u0000\u0000\u001a\u012a\u0001\u0000\u0000\u0000\u001c\u012c\u0001"+ - "\u0000\u0000\u0000\u001e\u012f\u0001\u0000\u0000\u0000 \u013a\u0001\u0000"+ - "\u0000\u0000\"\u013e\u0001\u0000\u0000\u0000$\u014d\u0001\u0000\u0000"+ - "\u0000&\u0151\u0001\u0000\u0000\u0000(\u0153\u0001\u0000\u0000\u0000*"+ - "\u0157\u0001\u0000\u0000\u0000,\u0159\u0001\u0000\u0000\u0000.\u0162\u0001"+ - "\u0000\u0000\u00000\u0166\u0001\u0000\u0000\u00002\u0176\u0001\u0000\u0000"+ - "\u00004\u0179\u0001\u0000\u0000\u00006\u0181\u0001\u0000\u0000\u00008"+ - "\u0189\u0001\u0000\u0000\u0000:\u018e\u0001\u0000\u0000\u0000<\u0196\u0001"+ - "\u0000\u0000\u0000>\u019e\u0001\u0000\u0000\u0000@\u01a6\u0001\u0000\u0000"+ - "\u0000B\u01ab\u0001\u0000\u0000\u0000D\u01d7\u0001\u0000\u0000\u0000F"+ - "\u01db\u0001\u0000\u0000\u0000H\u01e0\u0001\u0000\u0000\u0000J\u01e2\u0001"+ - "\u0000\u0000\u0000L\u01e5\u0001\u0000\u0000\u0000N\u01ee\u0001\u0000\u0000"+ - "\u0000P\u01f6\u0001\u0000\u0000\u0000R\u01f9\u0001\u0000\u0000\u0000T"+ - "\u01fc\u0001\u0000\u0000\u0000V\u0205\u0001\u0000\u0000\u0000X\u0209\u0001"+ - "\u0000\u0000\u0000Z\u020f\u0001\u0000\u0000\u0000\\\u0213\u0001\u0000"+ - "\u0000\u0000^\u0216\u0001\u0000\u0000\u0000`\u021e\u0001\u0000\u0000\u0000"+ - "b\u0222\u0001\u0000\u0000\u0000d\u0226\u0001\u0000\u0000\u0000f\u0229"+ - "\u0001\u0000\u0000\u0000h\u022e\u0001\u0000\u0000\u0000j\u0232\u0001\u0000"+ - "\u0000\u0000l\u0234\u0001\u0000\u0000\u0000n\u0236\u0001\u0000\u0000\u0000"+ - "p\u0239\u0001\u0000\u0000\u0000r\u023d\u0001\u0000\u0000\u0000t\u0240"+ - "\u0001\u0000\u0000\u0000v\u0254\u0001\u0000\u0000\u0000x\u0258\u0001\u0000"+ - "\u0000\u0000z\u025d\u0001\u0000\u0000\u0000|\u0264\u0001\u0000\u0000\u0000"+ - "~\u026a\u0001\u0000\u0000\u0000\u0080\u026f\u0001\u0000\u0000\u0000\u0082"+ - "\u0278\u0001\u0000\u0000\u0000\u0084\u0085\u0003\u0002\u0001\u0000\u0085"+ - "\u0086\u0005\u0000\u0000\u0001\u0086\u0001\u0001\u0000\u0000\u0000\u0087"+ - "\u0088\u0006\u0001\uffff\uffff\u0000\u0088\u0089\u0003\u0004\u0002\u0000"+ - "\u0089\u008f\u0001\u0000\u0000\u0000\u008a\u008b\n\u0001\u0000\u0000\u008b"+ - "\u008c\u0005\u001d\u0000\u0000\u008c\u008e\u0003\u0006\u0003\u0000\u008d"+ - "\u008a\u0001\u0000\u0000\u0000\u008e\u0091\u0001\u0000\u0000\u0000\u008f"+ - "\u008d\u0001\u0000\u0000\u0000\u008f\u0090\u0001\u0000\u0000\u0000\u0090"+ - "\u0003\u0001\u0000\u0000\u0000\u0091\u008f\u0001\u0000\u0000\u0000\u0092"+ - "\u0099\u0003n7\u0000\u0093\u0099\u0003\"\u0011\u0000\u0094\u0099\u0003"+ - "\u001c\u000e\u0000\u0095\u0099\u0003r9\u0000\u0096\u0097\u0004\u0002\u0001"+ - "\u0000\u0097\u0099\u00030\u0018\u0000\u0098\u0092\u0001\u0000\u0000\u0000"+ - "\u0098\u0093\u0001\u0000\u0000\u0000\u0098\u0094\u0001\u0000\u0000\u0000"+ - "\u0098\u0095\u0001\u0000\u0000\u0000\u0098\u0096\u0001\u0000\u0000\u0000"+ - "\u0099\u0005\u0001\u0000\u0000\u0000\u009a\u00ad\u00032\u0019\u0000\u009b"+ - "\u00ad\u0003\b\u0004\u0000\u009c\u00ad\u0003P(\u0000\u009d\u00ad\u0003"+ - "J%\u0000\u009e\u00ad\u00034\u001a\u0000\u009f\u00ad\u0003L&\u0000\u00a0"+ - "\u00ad\u0003R)\u0000\u00a1\u00ad\u0003T*\u0000\u00a2\u00ad\u0003X,\u0000"+ - "\u00a3\u00ad\u0003Z-\u0000\u00a4\u00ad\u0003t:\u0000\u00a5\u00ad\u0003"+ - "\\.\u0000\u00a6\u00a7\u0004\u0003\u0002\u0000\u00a7\u00ad\u0003z=\u0000"+ - "\u00a8\u00a9\u0004\u0003\u0003\u0000\u00a9\u00ad\u0003x<\u0000\u00aa\u00ab"+ - "\u0004\u0003\u0004\u0000\u00ab\u00ad\u0003|>\u0000\u00ac\u009a\u0001\u0000"+ - "\u0000\u0000\u00ac\u009b\u0001\u0000\u0000\u0000\u00ac\u009c\u0001\u0000"+ - "\u0000\u0000\u00ac\u009d\u0001\u0000\u0000\u0000\u00ac\u009e\u0001\u0000"+ - "\u0000\u0000\u00ac\u009f\u0001\u0000\u0000\u0000\u00ac\u00a0\u0001\u0000"+ - "\u0000\u0000\u00ac\u00a1\u0001\u0000\u0000\u0000\u00ac\u00a2\u0001\u0000"+ - "\u0000\u0000\u00ac\u00a3\u0001\u0000\u0000\u0000\u00ac\u00a4\u0001\u0000"+ - "\u0000\u0000\u00ac\u00a5\u0001\u0000\u0000\u0000\u00ac\u00a6\u0001\u0000"+ - "\u0000\u0000\u00ac\u00a8\u0001\u0000\u0000\u0000\u00ac\u00aa\u0001\u0000"+ - "\u0000\u0000\u00ad\u0007\u0001\u0000\u0000\u0000\u00ae\u00af\u0005\u0010"+ - "\u0000\u0000\u00af\u00b0\u0003\n\u0005\u0000\u00b0\t\u0001\u0000\u0000"+ - "\u0000\u00b1\u00b2\u0006\u0005\uffff\uffff\u0000\u00b2\u00b3\u00051\u0000"+ - "\u0000\u00b3\u00cf\u0003\n\u0005\b\u00b4\u00cf\u0003\u0010\b\u0000\u00b5"+ - "\u00cf\u0003\f\u0006\u0000\u00b6\u00b8\u0003\u0010\b\u0000\u00b7\u00b9"+ - "\u00051\u0000\u0000\u00b8\u00b7\u0001\u0000\u0000\u0000\u00b8\u00b9\u0001"+ - "\u0000\u0000\u0000\u00b9\u00ba\u0001\u0000\u0000\u0000\u00ba\u00bb\u0005"+ - ",\u0000\u0000\u00bb\u00bc\u00050\u0000\u0000\u00bc\u00c1\u0003\u0010\b"+ - "\u0000\u00bd\u00be\u0005\'\u0000\u0000\u00be\u00c0\u0003\u0010\b\u0000"+ - "\u00bf\u00bd\u0001\u0000\u0000\u0000\u00c0\u00c3\u0001\u0000\u0000\u0000"+ - "\u00c1\u00bf\u0001\u0000\u0000\u0000\u00c1\u00c2\u0001\u0000\u0000\u0000"+ - "\u00c2\u00c4\u0001\u0000\u0000\u0000\u00c3\u00c1\u0001\u0000\u0000\u0000"+ - "\u00c4\u00c5\u00057\u0000\u0000\u00c5\u00cf\u0001\u0000\u0000\u0000\u00c6"+ - "\u00c7\u0003\u0010\b\u0000\u00c7\u00c9\u0005-\u0000\u0000\u00c8\u00ca"+ - "\u00051\u0000\u0000\u00c9\u00c8\u0001\u0000\u0000\u0000\u00c9\u00ca\u0001"+ - "\u0000\u0000\u0000\u00ca\u00cb\u0001\u0000\u0000\u0000\u00cb\u00cc\u0005"+ - "2\u0000\u0000\u00cc\u00cf\u0001\u0000\u0000\u0000\u00cd\u00cf\u0003\u000e"+ - "\u0007\u0000\u00ce\u00b1\u0001\u0000\u0000\u0000\u00ce\u00b4\u0001\u0000"+ - "\u0000\u0000\u00ce\u00b5\u0001\u0000\u0000\u0000\u00ce\u00b6\u0001\u0000"+ - "\u0000\u0000\u00ce\u00c6\u0001\u0000\u0000\u0000\u00ce\u00cd\u0001\u0000"+ - "\u0000\u0000\u00cf\u00d8\u0001\u0000\u0000\u0000\u00d0\u00d1\n\u0005\u0000"+ - "\u0000\u00d1\u00d2\u0005\"\u0000\u0000\u00d2\u00d7\u0003\n\u0005\u0006"+ - "\u00d3\u00d4\n\u0004\u0000\u0000\u00d4\u00d5\u00054\u0000\u0000\u00d5"+ - "\u00d7\u0003\n\u0005\u0005\u00d6\u00d0\u0001\u0000\u0000\u0000\u00d6\u00d3"+ - "\u0001\u0000\u0000\u0000\u00d7\u00da\u0001\u0000\u0000\u0000\u00d8\u00d6"+ - "\u0001\u0000\u0000\u0000\u00d8\u00d9\u0001\u0000\u0000\u0000\u00d9\u000b"+ - "\u0001\u0000\u0000\u0000\u00da\u00d8\u0001\u0000\u0000\u0000\u00db\u00dd"+ - "\u0003\u0010\b\u0000\u00dc\u00de\u00051\u0000\u0000\u00dd\u00dc\u0001"+ - "\u0000\u0000\u0000\u00dd\u00de\u0001\u0000\u0000\u0000\u00de\u00df\u0001"+ - "\u0000\u0000\u0000\u00df\u00e0\u0005/\u0000\u0000\u00e0\u00e1\u0003j5"+ - "\u0000\u00e1\u00ea\u0001\u0000\u0000\u0000\u00e2\u00e4\u0003\u0010\b\u0000"+ - "\u00e3\u00e5\u00051\u0000\u0000\u00e4\u00e3\u0001\u0000\u0000\u0000\u00e4"+ - "\u00e5\u0001\u0000\u0000\u0000\u00e5\u00e6\u0001\u0000\u0000\u0000\u00e6"+ - "\u00e7\u00056\u0000\u0000\u00e7\u00e8\u0003j5\u0000\u00e8\u00ea\u0001"+ - "\u0000\u0000\u0000\u00e9\u00db\u0001\u0000\u0000\u0000\u00e9\u00e2\u0001"+ - "\u0000\u0000\u0000\u00ea\r\u0001\u0000\u0000\u0000\u00eb\u00ec\u0003:"+ - "\u001d\u0000\u00ec\u00ed\u0005&\u0000\u0000\u00ed\u00ee\u0003D\"\u0000"+ - "\u00ee\u000f\u0001\u0000\u0000\u0000\u00ef\u00f5\u0003\u0012\t\u0000\u00f0"+ - "\u00f1\u0003\u0012\t\u0000\u00f1\u00f2\u0003l6\u0000\u00f2\u00f3\u0003"+ - "\u0012\t\u0000\u00f3\u00f5\u0001\u0000\u0000\u0000\u00f4\u00ef\u0001\u0000"+ - "\u0000\u0000\u00f4\u00f0\u0001\u0000\u0000\u0000\u00f5\u0011\u0001\u0000"+ - "\u0000\u0000\u00f6\u00f7\u0006\t\uffff\uffff\u0000\u00f7\u00fb\u0003\u0014"+ - "\n\u0000\u00f8\u00f9\u0007\u0000\u0000\u0000\u00f9\u00fb\u0003\u0012\t"+ - "\u0003\u00fa\u00f6\u0001\u0000\u0000\u0000\u00fa\u00f8\u0001\u0000\u0000"+ - "\u0000\u00fb\u0104\u0001\u0000\u0000\u0000\u00fc\u00fd\n\u0002\u0000\u0000"+ - "\u00fd\u00fe\u0007\u0001\u0000\u0000\u00fe\u0103\u0003\u0012\t\u0003\u00ff"+ - "\u0100\n\u0001\u0000\u0000\u0100\u0101\u0007\u0000\u0000\u0000\u0101\u0103"+ - "\u0003\u0012\t\u0002\u0102\u00fc\u0001\u0000\u0000\u0000\u0102\u00ff\u0001"+ - "\u0000\u0000\u0000\u0103\u0106\u0001\u0000\u0000\u0000\u0104\u0102\u0001"+ - "\u0000\u0000\u0000\u0104\u0105\u0001\u0000\u0000\u0000\u0105\u0013\u0001"+ - "\u0000\u0000\u0000\u0106\u0104\u0001\u0000\u0000\u0000\u0107\u0108\u0006"+ - "\n\uffff\uffff\u0000\u0108\u0110\u0003D\"\u0000\u0109\u0110\u0003:\u001d"+ - "\u0000\u010a\u0110\u0003\u0016\u000b\u0000\u010b\u010c\u00050\u0000\u0000"+ - "\u010c\u010d\u0003\n\u0005\u0000\u010d\u010e\u00057\u0000\u0000\u010e"+ - "\u0110\u0001\u0000\u0000\u0000\u010f\u0107\u0001\u0000\u0000\u0000\u010f"+ - "\u0109\u0001\u0000\u0000\u0000\u010f\u010a\u0001\u0000\u0000\u0000\u010f"+ - "\u010b\u0001\u0000\u0000\u0000\u0110\u0116\u0001\u0000\u0000\u0000\u0111"+ - "\u0112\n\u0001\u0000\u0000\u0112\u0113\u0005%\u0000\u0000\u0113\u0115"+ - "\u0003\u001a\r\u0000\u0114\u0111\u0001\u0000\u0000\u0000\u0115\u0118\u0001"+ - "\u0000\u0000\u0000\u0116\u0114\u0001\u0000\u0000\u0000\u0116\u0117\u0001"+ - "\u0000\u0000\u0000\u0117\u0015\u0001\u0000\u0000\u0000\u0118\u0116\u0001"+ - "\u0000\u0000\u0000\u0119\u011a\u0003\u0018\f\u0000\u011a\u0124\u00050"+ - "\u0000\u0000\u011b\u0125\u0005B\u0000\u0000\u011c\u0121\u0003\n\u0005"+ - "\u0000\u011d\u011e\u0005\'\u0000\u0000\u011e\u0120\u0003\n\u0005\u0000"+ - "\u011f\u011d\u0001\u0000\u0000\u0000\u0120\u0123\u0001\u0000\u0000\u0000"+ - "\u0121\u011f\u0001\u0000\u0000\u0000\u0121\u0122\u0001\u0000\u0000\u0000"+ - "\u0122\u0125\u0001\u0000\u0000\u0000\u0123\u0121\u0001\u0000\u0000\u0000"+ - "\u0124\u011b\u0001\u0000\u0000\u0000\u0124\u011c\u0001\u0000\u0000\u0000"+ - "\u0124\u0125\u0001\u0000\u0000\u0000\u0125\u0126\u0001\u0000\u0000\u0000"+ - "\u0126\u0127\u00057\u0000\u0000\u0127\u0017\u0001\u0000\u0000\u0000\u0128"+ - "\u0129\u0003H$\u0000\u0129\u0019\u0001\u0000\u0000\u0000\u012a\u012b\u0003"+ - "@ \u0000\u012b\u001b\u0001\u0000\u0000\u0000\u012c\u012d\u0005\f\u0000"+ - "\u0000\u012d\u012e\u0003\u001e\u000f\u0000\u012e\u001d\u0001\u0000\u0000"+ - "\u0000\u012f\u0134\u0003 \u0010\u0000\u0130\u0131\u0005\'\u0000\u0000"+ - "\u0131\u0133\u0003 \u0010\u0000\u0132\u0130\u0001\u0000\u0000\u0000\u0133"+ - "\u0136\u0001\u0000\u0000\u0000\u0134\u0132\u0001\u0000\u0000\u0000\u0134"+ - "\u0135\u0001\u0000\u0000\u0000\u0135\u001f\u0001\u0000\u0000\u0000\u0136"+ - "\u0134\u0001\u0000\u0000\u0000\u0137\u0138\u0003:\u001d\u0000\u0138\u0139"+ - "\u0005$\u0000\u0000\u0139\u013b\u0001\u0000\u0000\u0000\u013a\u0137\u0001"+ - "\u0000\u0000\u0000\u013a\u013b\u0001\u0000\u0000\u0000\u013b\u013c\u0001"+ - "\u0000\u0000\u0000\u013c\u013d\u0003\n\u0005\u0000\u013d!\u0001\u0000"+ - "\u0000\u0000\u013e\u013f\u0005\u0006\u0000\u0000\u013f\u0144\u0003$\u0012"+ - "\u0000\u0140\u0141\u0005\'\u0000\u0000\u0141\u0143\u0003$\u0012\u0000"+ - "\u0142\u0140\u0001\u0000\u0000\u0000\u0143\u0146\u0001\u0000\u0000\u0000"+ - "\u0144\u0142\u0001\u0000\u0000\u0000\u0144\u0145\u0001\u0000\u0000\u0000"+ - "\u0145\u0148\u0001\u0000\u0000\u0000\u0146\u0144\u0001\u0000\u0000\u0000"+ - "\u0147\u0149\u0003*\u0015\u0000\u0148\u0147\u0001\u0000\u0000\u0000\u0148"+ - "\u0149\u0001\u0000\u0000\u0000\u0149#\u0001\u0000\u0000\u0000\u014a\u014b"+ - "\u0003&\u0013\u0000\u014b\u014c\u0005&\u0000\u0000\u014c\u014e\u0001\u0000"+ - "\u0000\u0000\u014d\u014a\u0001\u0000\u0000\u0000\u014d\u014e\u0001\u0000"+ - "\u0000\u0000\u014e\u014f\u0001\u0000\u0000\u0000\u014f\u0150\u0003(\u0014"+ - "\u0000\u0150%\u0001\u0000\u0000\u0000\u0151\u0152\u0005Q\u0000\u0000\u0152"+ - "\'\u0001\u0000\u0000\u0000\u0153\u0154\u0007\u0002\u0000\u0000\u0154)"+ - "\u0001\u0000\u0000\u0000\u0155\u0158\u0003,\u0016\u0000\u0156\u0158\u0003"+ - ".\u0017\u0000\u0157\u0155\u0001\u0000\u0000\u0000\u0157\u0156\u0001\u0000"+ - "\u0000\u0000\u0158+\u0001\u0000\u0000\u0000\u0159\u015a\u0005P\u0000\u0000"+ - "\u015a\u015f\u0005Q\u0000\u0000\u015b\u015c\u0005\'\u0000\u0000\u015c"+ - "\u015e\u0005Q\u0000\u0000\u015d\u015b\u0001\u0000\u0000\u0000\u015e\u0161"+ - "\u0001\u0000\u0000\u0000\u015f\u015d\u0001\u0000\u0000\u0000\u015f\u0160"+ - "\u0001\u0000\u0000\u0000\u0160-\u0001\u0000\u0000\u0000\u0161\u015f\u0001"+ - "\u0000\u0000\u0000\u0162\u0163\u0005F\u0000\u0000\u0163\u0164\u0003,\u0016"+ - "\u0000\u0164\u0165\u0005G\u0000\u0000\u0165/\u0001\u0000\u0000\u0000\u0166"+ - "\u0167\u0005\u0013\u0000\u0000\u0167\u016c\u0003$\u0012\u0000\u0168\u0169"+ - "\u0005\'\u0000\u0000\u0169\u016b\u0003$\u0012\u0000\u016a\u0168\u0001"+ - "\u0000\u0000\u0000\u016b\u016e\u0001\u0000\u0000\u0000\u016c\u016a\u0001"+ - "\u0000\u0000\u0000\u016c\u016d\u0001\u0000\u0000\u0000\u016d\u0170\u0001"+ - "\u0000\u0000\u0000\u016e\u016c\u0001\u0000\u0000\u0000\u016f\u0171\u0003"+ - "6\u001b\u0000\u0170\u016f\u0001\u0000\u0000\u0000\u0170\u0171\u0001\u0000"+ - "\u0000\u0000\u0171\u0174\u0001\u0000\u0000\u0000\u0172\u0173\u0005!\u0000"+ - "\u0000\u0173\u0175\u0003\u001e\u000f\u0000\u0174\u0172\u0001\u0000\u0000"+ - "\u0000\u0174\u0175\u0001\u0000\u0000\u0000\u01751\u0001\u0000\u0000\u0000"+ - "\u0176\u0177\u0005\u0004\u0000\u0000\u0177\u0178\u0003\u001e\u000f\u0000"+ - "\u01783\u0001\u0000\u0000\u0000\u0179\u017b\u0005\u000f\u0000\u0000\u017a"+ - "\u017c\u00036\u001b\u0000\u017b\u017a\u0001\u0000\u0000\u0000\u017b\u017c"+ - "\u0001\u0000\u0000\u0000\u017c\u017f\u0001\u0000\u0000\u0000\u017d\u017e"+ - "\u0005!\u0000\u0000\u017e\u0180\u0003\u001e\u000f\u0000\u017f\u017d\u0001"+ - "\u0000\u0000\u0000\u017f\u0180\u0001\u0000\u0000\u0000\u01805\u0001\u0000"+ - "\u0000\u0000\u0181\u0186\u00038\u001c\u0000\u0182\u0183\u0005\'\u0000"+ - "\u0000\u0183\u0185\u00038\u001c\u0000\u0184\u0182\u0001\u0000\u0000\u0000"+ - "\u0185\u0188\u0001\u0000\u0000\u0000\u0186\u0184\u0001\u0000\u0000\u0000"+ - "\u0186\u0187\u0001\u0000\u0000\u0000\u01877\u0001\u0000\u0000\u0000\u0188"+ - "\u0186\u0001\u0000\u0000\u0000\u0189\u018c\u0003 \u0010\u0000\u018a\u018b"+ - "\u0005\u0010\u0000\u0000\u018b\u018d\u0003\n\u0005\u0000\u018c\u018a\u0001"+ - "\u0000\u0000\u0000\u018c\u018d\u0001\u0000\u0000\u0000\u018d9\u0001\u0000"+ - "\u0000\u0000\u018e\u0193\u0003H$\u0000\u018f\u0190\u0005)\u0000\u0000"+ - "\u0190\u0192\u0003H$\u0000\u0191\u018f\u0001\u0000\u0000\u0000\u0192\u0195"+ - "\u0001\u0000\u0000\u0000\u0193\u0191\u0001\u0000\u0000\u0000\u0193\u0194"+ - "\u0001\u0000\u0000\u0000\u0194;\u0001\u0000\u0000\u0000\u0195\u0193\u0001"+ - "\u0000\u0000\u0000\u0196\u019b\u0003B!\u0000\u0197\u0198\u0005)\u0000"+ - "\u0000\u0198\u019a\u0003B!\u0000\u0199\u0197\u0001\u0000\u0000\u0000\u019a"+ - "\u019d\u0001\u0000\u0000\u0000\u019b\u0199\u0001\u0000\u0000\u0000\u019b"+ - "\u019c\u0001\u0000\u0000\u0000\u019c=\u0001\u0000\u0000\u0000\u019d\u019b"+ - "\u0001\u0000\u0000\u0000\u019e\u01a3\u0003<\u001e\u0000\u019f\u01a0\u0005"+ - "\'\u0000\u0000\u01a0\u01a2\u0003<\u001e\u0000\u01a1\u019f\u0001\u0000"+ - "\u0000\u0000\u01a2\u01a5\u0001\u0000\u0000\u0000\u01a3\u01a1\u0001\u0000"+ - "\u0000\u0000\u01a3\u01a4\u0001\u0000\u0000\u0000\u01a4?\u0001\u0000\u0000"+ - "\u0000\u01a5\u01a3\u0001\u0000\u0000\u0000\u01a6\u01a7\u0007\u0003\u0000"+ - "\u0000\u01a7A\u0001\u0000\u0000\u0000\u01a8\u01ac\u0005U\u0000\u0000\u01a9"+ - "\u01aa\u0004!\n\u0000\u01aa\u01ac\u0003F#\u0000\u01ab\u01a8\u0001\u0000"+ - "\u0000\u0000\u01ab\u01a9\u0001\u0000\u0000\u0000\u01acC\u0001\u0000\u0000"+ - "\u0000\u01ad\u01d8\u00052\u0000\u0000\u01ae\u01af\u0003h4\u0000\u01af"+ - "\u01b0\u0005H\u0000\u0000\u01b0\u01d8\u0001\u0000\u0000\u0000\u01b1\u01d8"+ - "\u0003f3\u0000\u01b2\u01d8\u0003h4\u0000\u01b3\u01d8\u0003b1\u0000\u01b4"+ - "\u01d8\u0003F#\u0000\u01b5\u01d8\u0003j5\u0000\u01b6\u01b7\u0005F\u0000"+ - "\u0000\u01b7\u01bc\u0003d2\u0000\u01b8\u01b9\u0005\'\u0000\u0000\u01b9"+ - "\u01bb\u0003d2\u0000\u01ba\u01b8\u0001\u0000\u0000\u0000\u01bb\u01be\u0001"+ - "\u0000\u0000\u0000\u01bc\u01ba\u0001\u0000\u0000\u0000\u01bc\u01bd\u0001"+ - "\u0000\u0000\u0000\u01bd\u01bf\u0001\u0000\u0000\u0000\u01be\u01bc\u0001"+ - "\u0000\u0000\u0000\u01bf\u01c0\u0005G\u0000\u0000\u01c0\u01d8\u0001\u0000"+ - "\u0000\u0000\u01c1\u01c2\u0005F\u0000\u0000\u01c2\u01c7\u0003b1\u0000"+ - "\u01c3\u01c4\u0005\'\u0000\u0000\u01c4\u01c6\u0003b1\u0000\u01c5\u01c3"+ - "\u0001\u0000\u0000\u0000\u01c6\u01c9\u0001\u0000\u0000\u0000\u01c7\u01c5"+ - "\u0001\u0000\u0000\u0000\u01c7\u01c8\u0001\u0000\u0000\u0000\u01c8\u01ca"+ - "\u0001\u0000\u0000\u0000\u01c9\u01c7\u0001\u0000\u0000\u0000\u01ca\u01cb"+ - "\u0005G\u0000\u0000\u01cb\u01d8\u0001\u0000\u0000\u0000\u01cc\u01cd\u0005"+ - "F\u0000\u0000\u01cd\u01d2\u0003j5\u0000\u01ce\u01cf\u0005\'\u0000\u0000"+ - "\u01cf\u01d1\u0003j5\u0000\u01d0\u01ce\u0001\u0000\u0000\u0000\u01d1\u01d4"+ - "\u0001\u0000\u0000\u0000\u01d2\u01d0\u0001\u0000\u0000\u0000\u01d2\u01d3"+ - "\u0001\u0000\u0000\u0000\u01d3\u01d5\u0001\u0000\u0000\u0000\u01d4\u01d2"+ - "\u0001\u0000\u0000\u0000\u01d5\u01d6\u0005G\u0000\u0000\u01d6\u01d8\u0001"+ - "\u0000\u0000\u0000\u01d7\u01ad\u0001\u0000\u0000\u0000\u01d7\u01ae\u0001"+ - "\u0000\u0000\u0000\u01d7\u01b1\u0001\u0000\u0000\u0000\u01d7\u01b2\u0001"+ - "\u0000\u0000\u0000\u01d7\u01b3\u0001\u0000\u0000\u0000\u01d7\u01b4\u0001"+ - "\u0000\u0000\u0000\u01d7\u01b5\u0001\u0000\u0000\u0000\u01d7\u01b6\u0001"+ - "\u0000\u0000\u0000\u01d7\u01c1\u0001\u0000\u0000\u0000\u01d7\u01cc\u0001"+ - "\u0000\u0000\u0000\u01d8E\u0001\u0000\u0000\u0000\u01d9\u01dc\u00055\u0000"+ - "\u0000\u01da\u01dc\u0005E\u0000\u0000\u01db\u01d9\u0001\u0000\u0000\u0000"+ - "\u01db\u01da\u0001\u0000\u0000\u0000\u01dcG\u0001\u0000\u0000\u0000\u01dd"+ - "\u01e1\u0003@ \u0000\u01de\u01df\u0004$\u000b\u0000\u01df\u01e1\u0003"+ - "F#\u0000\u01e0\u01dd\u0001\u0000\u0000\u0000\u01e0\u01de\u0001\u0000\u0000"+ - "\u0000\u01e1I\u0001\u0000\u0000\u0000\u01e2\u01e3\u0005\t\u0000\u0000"+ - "\u01e3\u01e4\u0005\u001f\u0000\u0000\u01e4K\u0001\u0000\u0000\u0000\u01e5"+ - "\u01e6\u0005\u000e\u0000\u0000\u01e6\u01eb\u0003N\'\u0000\u01e7\u01e8"+ - "\u0005\'\u0000\u0000\u01e8\u01ea\u0003N\'\u0000\u01e9\u01e7\u0001\u0000"+ - "\u0000\u0000\u01ea\u01ed\u0001\u0000\u0000\u0000\u01eb\u01e9\u0001\u0000"+ - "\u0000\u0000\u01eb\u01ec\u0001\u0000\u0000\u0000\u01ecM\u0001\u0000\u0000"+ - "\u0000\u01ed\u01eb\u0001\u0000\u0000\u0000\u01ee\u01f0\u0003\n\u0005\u0000"+ - "\u01ef\u01f1\u0007\u0004\u0000\u0000\u01f0\u01ef\u0001\u0000\u0000\u0000"+ - "\u01f0\u01f1\u0001\u0000\u0000\u0000\u01f1\u01f4\u0001\u0000\u0000\u0000"+ - "\u01f2\u01f3\u00053\u0000\u0000\u01f3\u01f5\u0007\u0005\u0000\u0000\u01f4"+ - "\u01f2\u0001\u0000\u0000\u0000\u01f4\u01f5\u0001\u0000\u0000\u0000\u01f5"+ - "O\u0001\u0000\u0000\u0000\u01f6\u01f7\u0005\b\u0000\u0000\u01f7\u01f8"+ - "\u0003>\u001f\u0000\u01f8Q\u0001\u0000\u0000\u0000\u01f9\u01fa\u0005\u0002"+ - "\u0000\u0000\u01fa\u01fb\u0003>\u001f\u0000\u01fbS\u0001\u0000\u0000\u0000"+ - "\u01fc\u01fd\u0005\u000b\u0000\u0000\u01fd\u0202\u0003V+\u0000\u01fe\u01ff"+ - "\u0005\'\u0000\u0000\u01ff\u0201\u0003V+\u0000\u0200\u01fe\u0001\u0000"+ - "\u0000\u0000\u0201\u0204\u0001\u0000\u0000\u0000\u0202\u0200\u0001\u0000"+ - "\u0000\u0000\u0202\u0203\u0001\u0000\u0000\u0000\u0203U\u0001\u0000\u0000"+ - "\u0000\u0204\u0202\u0001\u0000\u0000\u0000\u0205\u0206\u0003<\u001e\u0000"+ - "\u0206\u0207\u0005Y\u0000\u0000\u0207\u0208\u0003<\u001e\u0000\u0208W"+ - "\u0001\u0000\u0000\u0000\u0209\u020a\u0005\u0001\u0000\u0000\u020a\u020b"+ - "\u0003\u0014\n\u0000\u020b\u020d\u0003j5\u0000\u020c\u020e\u0003^/\u0000"+ - "\u020d\u020c\u0001\u0000\u0000\u0000\u020d\u020e\u0001\u0000\u0000\u0000"+ - "\u020eY\u0001\u0000\u0000\u0000\u020f\u0210\u0005\u0007\u0000\u0000\u0210"+ - "\u0211\u0003\u0014\n\u0000\u0211\u0212\u0003j5\u0000\u0212[\u0001\u0000"+ - "\u0000\u0000\u0213\u0214\u0005\n\u0000\u0000\u0214\u0215\u0003:\u001d"+ - "\u0000\u0215]\u0001\u0000\u0000\u0000\u0216\u021b\u0003`0\u0000\u0217"+ - "\u0218\u0005\'\u0000\u0000\u0218\u021a\u0003`0\u0000\u0219\u0217\u0001"+ - "\u0000\u0000\u0000\u021a\u021d\u0001\u0000\u0000\u0000\u021b\u0219\u0001"+ - "\u0000\u0000\u0000\u021b\u021c\u0001\u0000\u0000\u0000\u021c_\u0001\u0000"+ - "\u0000\u0000\u021d\u021b\u0001\u0000\u0000\u0000\u021e\u021f\u0003@ \u0000"+ - "\u021f\u0220\u0005$\u0000\u0000\u0220\u0221\u0003D\"\u0000\u0221a\u0001"+ - "\u0000\u0000\u0000\u0222\u0223\u0007\u0006\u0000\u0000\u0223c\u0001\u0000"+ - "\u0000\u0000\u0224\u0227\u0003f3\u0000\u0225\u0227\u0003h4\u0000\u0226"+ - "\u0224\u0001\u0000\u0000\u0000\u0226\u0225\u0001\u0000\u0000\u0000\u0227"+ - "e\u0001\u0000\u0000\u0000\u0228\u022a\u0007\u0000\u0000\u0000\u0229\u0228"+ - "\u0001\u0000\u0000\u0000\u0229\u022a\u0001\u0000\u0000\u0000\u022a\u022b"+ - "\u0001\u0000\u0000\u0000\u022b\u022c\u0005 \u0000\u0000\u022cg\u0001\u0000"+ - "\u0000\u0000\u022d\u022f\u0007\u0000\u0000\u0000\u022e\u022d\u0001\u0000"+ - "\u0000\u0000\u022e\u022f\u0001\u0000\u0000\u0000\u022f\u0230\u0001\u0000"+ - "\u0000\u0000\u0230\u0231\u0005\u001f\u0000\u0000\u0231i\u0001\u0000\u0000"+ - "\u0000\u0232\u0233\u0005\u001e\u0000\u0000\u0233k\u0001\u0000\u0000\u0000"+ - "\u0234\u0235\u0007\u0007\u0000\u0000\u0235m\u0001\u0000\u0000\u0000\u0236"+ - "\u0237\u0005\u0005\u0000\u0000\u0237\u0238\u0003p8\u0000\u0238o\u0001"+ - "\u0000\u0000\u0000\u0239\u023a\u0005F\u0000\u0000\u023a\u023b\u0003\u0002"+ - "\u0001\u0000\u023b\u023c\u0005G\u0000\u0000\u023cq\u0001\u0000\u0000\u0000"+ - "\u023d\u023e\u0005\r\u0000\u0000\u023e\u023f\u0005i\u0000\u0000\u023f"+ - "s\u0001\u0000\u0000\u0000\u0240\u0241\u0005\u0003\u0000\u0000\u0241\u0244"+ - "\u0005_\u0000\u0000\u0242\u0243\u0005]\u0000\u0000\u0243\u0245\u0003<"+ - "\u001e\u0000\u0244\u0242\u0001\u0000\u0000\u0000\u0244\u0245\u0001\u0000"+ - "\u0000\u0000\u0245\u024f\u0001\u0000\u0000\u0000\u0246\u0247\u0005^\u0000"+ - "\u0000\u0247\u024c\u0003v;\u0000\u0248\u0249\u0005\'\u0000\u0000\u0249"+ - "\u024b\u0003v;\u0000\u024a\u0248\u0001\u0000\u0000\u0000\u024b\u024e\u0001"+ - "\u0000\u0000\u0000\u024c\u024a\u0001\u0000\u0000\u0000\u024c\u024d\u0001"+ - "\u0000\u0000\u0000\u024d\u0250\u0001\u0000\u0000\u0000\u024e\u024c\u0001"+ - "\u0000\u0000\u0000\u024f\u0246\u0001\u0000\u0000\u0000\u024f\u0250\u0001"+ - "\u0000\u0000\u0000\u0250u\u0001\u0000\u0000\u0000\u0251\u0252\u0003<\u001e"+ - "\u0000\u0252\u0253\u0005$\u0000\u0000\u0253\u0255\u0001\u0000\u0000\u0000"+ - "\u0254\u0251\u0001\u0000\u0000\u0000\u0254\u0255\u0001\u0000\u0000\u0000"+ - "\u0255\u0256\u0001\u0000\u0000\u0000\u0256\u0257\u0003<\u001e\u0000\u0257"+ - "w\u0001\u0000\u0000\u0000\u0258\u0259\u0005\u0012\u0000\u0000\u0259\u025a"+ - "\u0003$\u0012\u0000\u025a\u025b\u0005]\u0000\u0000\u025b\u025c\u0003>"+ - "\u001f\u0000\u025cy\u0001\u0000\u0000\u0000\u025d\u025e\u0005\u0011\u0000"+ - "\u0000\u025e\u0261\u00036\u001b\u0000\u025f\u0260\u0005!\u0000\u0000\u0260"+ - "\u0262\u0003\u001e\u000f\u0000\u0261\u025f\u0001\u0000\u0000\u0000\u0261"+ - "\u0262\u0001\u0000\u0000\u0000\u0262{\u0001\u0000\u0000\u0000\u0263\u0265"+ - "\u0007\b\u0000\u0000\u0264\u0263\u0001\u0000\u0000\u0000\u0264\u0265\u0001"+ - "\u0000\u0000\u0000\u0265\u0266\u0001\u0000\u0000\u0000\u0266\u0267\u0005"+ - "\u0014\u0000\u0000\u0267\u0268\u0003~?\u0000\u0268\u0269\u0003\u0080@"+ - "\u0000\u0269}\u0001\u0000\u0000\u0000\u026a\u026d\u0003@ \u0000\u026b"+ - "\u026c\u0005Y\u0000\u0000\u026c\u026e\u0003@ \u0000\u026d\u026b\u0001"+ - "\u0000\u0000\u0000\u026d\u026e\u0001\u0000\u0000\u0000\u026e\u007f\u0001"+ - "\u0000\u0000\u0000\u026f\u0270\u0005]\u0000\u0000\u0270\u0275\u0003\u0082"+ - "A\u0000\u0271\u0272\u0005\'\u0000\u0000\u0272\u0274\u0003\u0082A\u0000"+ - "\u0273\u0271\u0001\u0000\u0000\u0000\u0274\u0277\u0001\u0000\u0000\u0000"+ - "\u0275\u0273\u0001\u0000\u0000\u0000\u0275\u0276\u0001\u0000\u0000\u0000"+ - "\u0276\u0081\u0001\u0000\u0000\u0000\u0277\u0275\u0001\u0000\u0000\u0000"+ - "\u0278\u0279\u0003\u0010\b\u0000\u0279\u0083\u0001\u0000\u0000\u0000="+ - "\u008f\u0098\u00ac\u00b8\u00c1\u00c9\u00ce\u00d6\u00d8\u00dd\u00e4\u00e9"+ - "\u00f4\u00fa\u0102\u0104\u010f\u0116\u0121\u0124\u0134\u013a\u0144\u0148"+ - "\u014d\u0157\u015f\u016c\u0170\u0174\u017b\u017f\u0186\u018c\u0193\u019b"+ - "\u01a3\u01ab\u01bc\u01c7\u01d2\u01d7\u01db\u01e0\u01eb\u01f0\u01f4\u0202"+ - "\u020d\u021b\u0226\u0229\u022e\u0244\u024c\u024f\u0254\u0261\u0264\u026d"+ - "\u0275"; + "\u00ea\b\u0006\u0001\u0007\u0001\u0007\u0001\u0007\u0003\u0007\u00ef\b"+ + "\u0007\u0001\u0007\u0001\u0007\u0001\u0007\u0001\b\u0001\b\u0001\b\u0001"+ + "\b\u0001\b\u0003\b\u00f9\b\b\u0001\t\u0001\t\u0001\t\u0001\t\u0003\t\u00ff"+ + "\b\t\u0001\t\u0001\t\u0001\t\u0001\t\u0001\t\u0001\t\u0005\t\u0107\b\t"+ + "\n\t\f\t\u010a\t\t\u0001\n\u0001\n\u0001\n\u0001\n\u0001\n\u0001\n\u0001"+ + "\n\u0001\n\u0003\n\u0114\b\n\u0001\n\u0001\n\u0001\n\u0005\n\u0119\b\n"+ + "\n\n\f\n\u011c\t\n\u0001\u000b\u0001\u000b\u0001\u000b\u0001\u000b\u0001"+ + "\u000b\u0001\u000b\u0005\u000b\u0124\b\u000b\n\u000b\f\u000b\u0127\t\u000b"+ + "\u0003\u000b\u0129\b\u000b\u0001\u000b\u0001\u000b\u0001\f\u0001\f\u0001"+ + "\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000e\u0001\u000f\u0001\u000f"+ + "\u0001\u000f\u0005\u000f\u0137\b\u000f\n\u000f\f\u000f\u013a\t\u000f\u0001"+ + "\u0010\u0001\u0010\u0001\u0010\u0003\u0010\u013f\b\u0010\u0001\u0010\u0001"+ + "\u0010\u0001\u0011\u0001\u0011\u0001\u0011\u0001\u0011\u0005\u0011\u0147"+ + "\b\u0011\n\u0011\f\u0011\u014a\t\u0011\u0001\u0011\u0003\u0011\u014d\b"+ + "\u0011\u0001\u0012\u0001\u0012\u0001\u0012\u0003\u0012\u0152\b\u0012\u0001"+ + "\u0012\u0001\u0012\u0001\u0013\u0001\u0013\u0001\u0014\u0001\u0014\u0001"+ + "\u0015\u0001\u0015\u0003\u0015\u015c\b\u0015\u0001\u0016\u0001\u0016\u0001"+ + "\u0016\u0001\u0016\u0005\u0016\u0162\b\u0016\n\u0016\f\u0016\u0165\t\u0016"+ + "\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0018\u0001\u0018"+ + "\u0001\u0018\u0001\u0018\u0005\u0018\u016f\b\u0018\n\u0018\f\u0018\u0172"+ + "\t\u0018\u0001\u0018\u0003\u0018\u0175\b\u0018\u0001\u0018\u0001\u0018"+ + "\u0003\u0018\u0179\b\u0018\u0001\u0019\u0001\u0019\u0001\u0019\u0001\u001a"+ + "\u0001\u001a\u0003\u001a\u0180\b\u001a\u0001\u001a\u0001\u001a\u0003\u001a"+ + "\u0184\b\u001a\u0001\u001b\u0001\u001b\u0001\u001b\u0005\u001b\u0189\b"+ + "\u001b\n\u001b\f\u001b\u018c\t\u001b\u0001\u001c\u0001\u001c\u0001\u001c"+ + "\u0003\u001c\u0191\b\u001c\u0001\u001d\u0001\u001d\u0001\u001d\u0005\u001d"+ + "\u0196\b\u001d\n\u001d\f\u001d\u0199\t\u001d\u0001\u001e\u0001\u001e\u0001"+ + "\u001e\u0005\u001e\u019e\b\u001e\n\u001e\f\u001e\u01a1\t\u001e\u0001\u001f"+ + "\u0001\u001f\u0001\u001f\u0005\u001f\u01a6\b\u001f\n\u001f\f\u001f\u01a9"+ + "\t\u001f\u0001 \u0001 \u0001!\u0001!\u0001!\u0003!\u01b0\b!\u0001\"\u0001"+ + "\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001"+ + "\"\u0001\"\u0001\"\u0005\"\u01bf\b\"\n\"\f\"\u01c2\t\"\u0001\"\u0001\""+ + "\u0001\"\u0001\"\u0001\"\u0001\"\u0005\"\u01ca\b\"\n\"\f\"\u01cd\t\"\u0001"+ + "\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0005\"\u01d5\b\"\n\"\f\"\u01d8"+ + "\t\"\u0001\"\u0001\"\u0003\"\u01dc\b\"\u0001#\u0001#\u0003#\u01e0\b#\u0001"+ + "$\u0001$\u0001$\u0003$\u01e5\b$\u0001%\u0001%\u0001%\u0001&\u0001&\u0001"+ + "&\u0001&\u0005&\u01ee\b&\n&\f&\u01f1\t&\u0001\'\u0001\'\u0003\'\u01f5"+ + "\b\'\u0001\'\u0001\'\u0003\'\u01f9\b\'\u0001(\u0001(\u0001(\u0001)\u0001"+ + ")\u0001)\u0001*\u0001*\u0001*\u0001*\u0005*\u0205\b*\n*\f*\u0208\t*\u0001"+ + "+\u0001+\u0001+\u0001+\u0001,\u0001,\u0001,\u0001,\u0003,\u0212\b,\u0001"+ + "-\u0001-\u0001-\u0001-\u0001.\u0001.\u0001.\u0001/\u0001/\u0001/\u0005"+ + "/\u021e\b/\n/\f/\u0221\t/\u00010\u00010\u00010\u00010\u00011\u00011\u0001"+ + "2\u00012\u00032\u022b\b2\u00013\u00033\u022e\b3\u00013\u00013\u00014\u0003"+ + "4\u0233\b4\u00014\u00014\u00015\u00015\u00016\u00016\u00017\u00017\u0001"+ + "7\u00018\u00018\u00018\u00018\u00019\u00019\u00019\u0001:\u0001:\u0001"+ + ":\u0001:\u0003:\u0249\b:\u0001:\u0001:\u0001:\u0001:\u0005:\u024f\b:\n"+ + ":\f:\u0252\t:\u0003:\u0254\b:\u0001;\u0001;\u0001;\u0003;\u0259\b;\u0001"+ + ";\u0001;\u0001<\u0001<\u0001<\u0001<\u0001<\u0001=\u0001=\u0001=\u0001"+ + "=\u0003=\u0266\b=\u0001>\u0003>\u0269\b>\u0001>\u0001>\u0001>\u0001>\u0001"+ + "?\u0001?\u0001?\u0003?\u0272\b?\u0001@\u0001@\u0001@\u0001@\u0005@\u0278"+ + "\b@\n@\f@\u027b\t@\u0001A\u0001A\u0001A\u0000\u0004\u0002\n\u0012\u0014"+ + "B\u0000\u0002\u0004\u0006\b\n\f\u000e\u0010\u0012\u0014\u0016\u0018\u001a"+ + "\u001c\u001e \"$&(*,.02468:<>@BDFHJLNPRTVXZ\\^`bdfhjlnprtvxz|~\u0080\u0082"+ + "\u0000\t\u0001\u0000@A\u0001\u0000BD\u0002\u0000\u001e\u001eQQ\u0001\u0000"+ + "HI\u0002\u0000##((\u0002\u0000++..\u0002\u0000**88\u0002\u000099;?\u0001"+ + "\u0000\u0016\u0018\u0299\u0000\u0084\u0001\u0000\u0000\u0000\u0002\u0087"+ + "\u0001\u0000\u0000\u0000\u0004\u0098\u0001\u0000\u0000\u0000\u0006\u00ac"+ + "\u0001\u0000\u0000\u0000\b\u00ae\u0001\u0000\u0000\u0000\n\u00ce\u0001"+ + "\u0000\u0000\u0000\f\u00e9\u0001\u0000\u0000\u0000\u000e\u00eb\u0001\u0000"+ + "\u0000\u0000\u0010\u00f8\u0001\u0000\u0000\u0000\u0012\u00fe\u0001\u0000"+ + "\u0000\u0000\u0014\u0113\u0001\u0000\u0000\u0000\u0016\u011d\u0001\u0000"+ + "\u0000\u0000\u0018\u012c\u0001\u0000\u0000\u0000\u001a\u012e\u0001\u0000"+ + "\u0000\u0000\u001c\u0130\u0001\u0000\u0000\u0000\u001e\u0133\u0001\u0000"+ + "\u0000\u0000 \u013e\u0001\u0000\u0000\u0000\"\u0142\u0001\u0000\u0000"+ + "\u0000$\u0151\u0001\u0000\u0000\u0000&\u0155\u0001\u0000\u0000\u0000("+ + "\u0157\u0001\u0000\u0000\u0000*\u015b\u0001\u0000\u0000\u0000,\u015d\u0001"+ + "\u0000\u0000\u0000.\u0166\u0001\u0000\u0000\u00000\u016a\u0001\u0000\u0000"+ + "\u00002\u017a\u0001\u0000\u0000\u00004\u017d\u0001\u0000\u0000\u00006"+ + "\u0185\u0001\u0000\u0000\u00008\u018d\u0001\u0000\u0000\u0000:\u0192\u0001"+ + "\u0000\u0000\u0000<\u019a\u0001\u0000\u0000\u0000>\u01a2\u0001\u0000\u0000"+ + "\u0000@\u01aa\u0001\u0000\u0000\u0000B\u01af\u0001\u0000\u0000\u0000D"+ + "\u01db\u0001\u0000\u0000\u0000F\u01df\u0001\u0000\u0000\u0000H\u01e4\u0001"+ + "\u0000\u0000\u0000J\u01e6\u0001\u0000\u0000\u0000L\u01e9\u0001\u0000\u0000"+ + "\u0000N\u01f2\u0001\u0000\u0000\u0000P\u01fa\u0001\u0000\u0000\u0000R"+ + "\u01fd\u0001\u0000\u0000\u0000T\u0200\u0001\u0000\u0000\u0000V\u0209\u0001"+ + "\u0000\u0000\u0000X\u020d\u0001\u0000\u0000\u0000Z\u0213\u0001\u0000\u0000"+ + "\u0000\\\u0217\u0001\u0000\u0000\u0000^\u021a\u0001\u0000\u0000\u0000"+ + "`\u0222\u0001\u0000\u0000\u0000b\u0226\u0001\u0000\u0000\u0000d\u022a"+ + "\u0001\u0000\u0000\u0000f\u022d\u0001\u0000\u0000\u0000h\u0232\u0001\u0000"+ + "\u0000\u0000j\u0236\u0001\u0000\u0000\u0000l\u0238\u0001\u0000\u0000\u0000"+ + "n\u023a\u0001\u0000\u0000\u0000p\u023d\u0001\u0000\u0000\u0000r\u0241"+ + "\u0001\u0000\u0000\u0000t\u0244\u0001\u0000\u0000\u0000v\u0258\u0001\u0000"+ + "\u0000\u0000x\u025c\u0001\u0000\u0000\u0000z\u0261\u0001\u0000\u0000\u0000"+ + "|\u0268\u0001\u0000\u0000\u0000~\u026e\u0001\u0000\u0000\u0000\u0080\u0273"+ + "\u0001\u0000\u0000\u0000\u0082\u027c\u0001\u0000\u0000\u0000\u0084\u0085"+ + "\u0003\u0002\u0001\u0000\u0085\u0086\u0005\u0000\u0000\u0001\u0086\u0001"+ + "\u0001\u0000\u0000\u0000\u0087\u0088\u0006\u0001\uffff\uffff\u0000\u0088"+ + "\u0089\u0003\u0004\u0002\u0000\u0089\u008f\u0001\u0000\u0000\u0000\u008a"+ + "\u008b\n\u0001\u0000\u0000\u008b\u008c\u0005\u001d\u0000\u0000\u008c\u008e"+ + "\u0003\u0006\u0003\u0000\u008d\u008a\u0001\u0000\u0000\u0000\u008e\u0091"+ + "\u0001\u0000\u0000\u0000\u008f\u008d\u0001\u0000\u0000\u0000\u008f\u0090"+ + "\u0001\u0000\u0000\u0000\u0090\u0003\u0001\u0000\u0000\u0000\u0091\u008f"+ + "\u0001\u0000\u0000\u0000\u0092\u0099\u0003n7\u0000\u0093\u0099\u0003\""+ + "\u0011\u0000\u0094\u0099\u0003\u001c\u000e\u0000\u0095\u0099\u0003r9\u0000"+ + "\u0096\u0097\u0004\u0002\u0001\u0000\u0097\u0099\u00030\u0018\u0000\u0098"+ + "\u0092\u0001\u0000\u0000\u0000\u0098\u0093\u0001\u0000\u0000\u0000\u0098"+ + "\u0094\u0001\u0000\u0000\u0000\u0098\u0095\u0001\u0000\u0000\u0000\u0098"+ + "\u0096\u0001\u0000\u0000\u0000\u0099\u0005\u0001\u0000\u0000\u0000\u009a"+ + "\u00ad\u00032\u0019\u0000\u009b\u00ad\u0003\b\u0004\u0000\u009c\u00ad"+ + "\u0003P(\u0000\u009d\u00ad\u0003J%\u0000\u009e\u00ad\u00034\u001a\u0000"+ + "\u009f\u00ad\u0003L&\u0000\u00a0\u00ad\u0003R)\u0000\u00a1\u00ad\u0003"+ + "T*\u0000\u00a2\u00ad\u0003X,\u0000\u00a3\u00ad\u0003Z-\u0000\u00a4\u00ad"+ + "\u0003t:\u0000\u00a5\u00ad\u0003\\.\u0000\u00a6\u00a7\u0004\u0003\u0002"+ + "\u0000\u00a7\u00ad\u0003z=\u0000\u00a8\u00a9\u0004\u0003\u0003\u0000\u00a9"+ + "\u00ad\u0003x<\u0000\u00aa\u00ab\u0004\u0003\u0004\u0000\u00ab\u00ad\u0003"+ + "|>\u0000\u00ac\u009a\u0001\u0000\u0000\u0000\u00ac\u009b\u0001\u0000\u0000"+ + "\u0000\u00ac\u009c\u0001\u0000\u0000\u0000\u00ac\u009d\u0001\u0000\u0000"+ + "\u0000\u00ac\u009e\u0001\u0000\u0000\u0000\u00ac\u009f\u0001\u0000\u0000"+ + "\u0000\u00ac\u00a0\u0001\u0000\u0000\u0000\u00ac\u00a1\u0001\u0000\u0000"+ + "\u0000\u00ac\u00a2\u0001\u0000\u0000\u0000\u00ac\u00a3\u0001\u0000\u0000"+ + "\u0000\u00ac\u00a4\u0001\u0000\u0000\u0000\u00ac\u00a5\u0001\u0000\u0000"+ + "\u0000\u00ac\u00a6\u0001\u0000\u0000\u0000\u00ac\u00a8\u0001\u0000\u0000"+ + "\u0000\u00ac\u00aa\u0001\u0000\u0000\u0000\u00ad\u0007\u0001\u0000\u0000"+ + "\u0000\u00ae\u00af\u0005\u0010\u0000\u0000\u00af\u00b0\u0003\n\u0005\u0000"+ + "\u00b0\t\u0001\u0000\u0000\u0000\u00b1\u00b2\u0006\u0005\uffff\uffff\u0000"+ + "\u00b2\u00b3\u00051\u0000\u0000\u00b3\u00cf\u0003\n\u0005\b\u00b4\u00cf"+ + "\u0003\u0010\b\u0000\u00b5\u00cf\u0003\f\u0006\u0000\u00b6\u00b8\u0003"+ + "\u0010\b\u0000\u00b7\u00b9\u00051\u0000\u0000\u00b8\u00b7\u0001\u0000"+ + "\u0000\u0000\u00b8\u00b9\u0001\u0000\u0000\u0000\u00b9\u00ba\u0001\u0000"+ + "\u0000\u0000\u00ba\u00bb\u0005,\u0000\u0000\u00bb\u00bc\u00050\u0000\u0000"+ + "\u00bc\u00c1\u0003\u0010\b\u0000\u00bd\u00be\u0005\'\u0000\u0000\u00be"+ + "\u00c0\u0003\u0010\b\u0000\u00bf\u00bd\u0001\u0000\u0000\u0000\u00c0\u00c3"+ + "\u0001\u0000\u0000\u0000\u00c1\u00bf\u0001\u0000\u0000\u0000\u00c1\u00c2"+ + "\u0001\u0000\u0000\u0000\u00c2\u00c4\u0001\u0000\u0000\u0000\u00c3\u00c1"+ + "\u0001\u0000\u0000\u0000\u00c4\u00c5\u00057\u0000\u0000\u00c5\u00cf\u0001"+ + "\u0000\u0000\u0000\u00c6\u00c7\u0003\u0010\b\u0000\u00c7\u00c9\u0005-"+ + "\u0000\u0000\u00c8\u00ca\u00051\u0000\u0000\u00c9\u00c8\u0001\u0000\u0000"+ + "\u0000\u00c9\u00ca\u0001\u0000\u0000\u0000\u00ca\u00cb\u0001\u0000\u0000"+ + "\u0000\u00cb\u00cc\u00052\u0000\u0000\u00cc\u00cf\u0001\u0000\u0000\u0000"+ + "\u00cd\u00cf\u0003\u000e\u0007\u0000\u00ce\u00b1\u0001\u0000\u0000\u0000"+ + "\u00ce\u00b4\u0001\u0000\u0000\u0000\u00ce\u00b5\u0001\u0000\u0000\u0000"+ + "\u00ce\u00b6\u0001\u0000\u0000\u0000\u00ce\u00c6\u0001\u0000\u0000\u0000"+ + "\u00ce\u00cd\u0001\u0000\u0000\u0000\u00cf\u00d8\u0001\u0000\u0000\u0000"+ + "\u00d0\u00d1\n\u0005\u0000\u0000\u00d1\u00d2\u0005\"\u0000\u0000\u00d2"+ + "\u00d7\u0003\n\u0005\u0006\u00d3\u00d4\n\u0004\u0000\u0000\u00d4\u00d5"+ + "\u00054\u0000\u0000\u00d5\u00d7\u0003\n\u0005\u0005\u00d6\u00d0\u0001"+ + "\u0000\u0000\u0000\u00d6\u00d3\u0001\u0000\u0000\u0000\u00d7\u00da\u0001"+ + "\u0000\u0000\u0000\u00d8\u00d6\u0001\u0000\u0000\u0000\u00d8\u00d9\u0001"+ + "\u0000\u0000\u0000\u00d9\u000b\u0001\u0000\u0000\u0000\u00da\u00d8\u0001"+ + "\u0000\u0000\u0000\u00db\u00dd\u0003\u0010\b\u0000\u00dc\u00de\u00051"+ + "\u0000\u0000\u00dd\u00dc\u0001\u0000\u0000\u0000\u00dd\u00de\u0001\u0000"+ + "\u0000\u0000\u00de\u00df\u0001\u0000\u0000\u0000\u00df\u00e0\u0005/\u0000"+ + "\u0000\u00e0\u00e1\u0003j5\u0000\u00e1\u00ea\u0001\u0000\u0000\u0000\u00e2"+ + "\u00e4\u0003\u0010\b\u0000\u00e3\u00e5\u00051\u0000\u0000\u00e4\u00e3"+ + "\u0001\u0000\u0000\u0000\u00e4\u00e5\u0001\u0000\u0000\u0000\u00e5\u00e6"+ + "\u0001\u0000\u0000\u0000\u00e6\u00e7\u00056\u0000\u0000\u00e7\u00e8\u0003"+ + "j5\u0000\u00e8\u00ea\u0001\u0000\u0000\u0000\u00e9\u00db\u0001\u0000\u0000"+ + "\u0000\u00e9\u00e2\u0001\u0000\u0000\u0000\u00ea\r\u0001\u0000\u0000\u0000"+ + "\u00eb\u00ee\u0003:\u001d\u0000\u00ec\u00ed\u0005%\u0000\u0000\u00ed\u00ef"+ + "\u0003\u001a\r\u0000\u00ee\u00ec\u0001\u0000\u0000\u0000\u00ee\u00ef\u0001"+ + "\u0000\u0000\u0000\u00ef\u00f0\u0001\u0000\u0000\u0000\u00f0\u00f1\u0005"+ + "&\u0000\u0000\u00f1\u00f2\u0003D\"\u0000\u00f2\u000f\u0001\u0000\u0000"+ + "\u0000\u00f3\u00f9\u0003\u0012\t\u0000\u00f4\u00f5\u0003\u0012\t\u0000"+ + "\u00f5\u00f6\u0003l6\u0000\u00f6\u00f7\u0003\u0012\t\u0000\u00f7\u00f9"+ + "\u0001\u0000\u0000\u0000\u00f8\u00f3\u0001\u0000\u0000\u0000\u00f8\u00f4"+ + "\u0001\u0000\u0000\u0000\u00f9\u0011\u0001\u0000\u0000\u0000\u00fa\u00fb"+ + "\u0006\t\uffff\uffff\u0000\u00fb\u00ff\u0003\u0014\n\u0000\u00fc\u00fd"+ + "\u0007\u0000\u0000\u0000\u00fd\u00ff\u0003\u0012\t\u0003\u00fe\u00fa\u0001"+ + "\u0000\u0000\u0000\u00fe\u00fc\u0001\u0000\u0000\u0000\u00ff\u0108\u0001"+ + "\u0000\u0000\u0000\u0100\u0101\n\u0002\u0000\u0000\u0101\u0102\u0007\u0001"+ + "\u0000\u0000\u0102\u0107\u0003\u0012\t\u0003\u0103\u0104\n\u0001\u0000"+ + "\u0000\u0104\u0105\u0007\u0000\u0000\u0000\u0105\u0107\u0003\u0012\t\u0002"+ + "\u0106\u0100\u0001\u0000\u0000\u0000\u0106\u0103\u0001\u0000\u0000\u0000"+ + "\u0107\u010a\u0001\u0000\u0000\u0000\u0108\u0106\u0001\u0000\u0000\u0000"+ + "\u0108\u0109\u0001\u0000\u0000\u0000\u0109\u0013\u0001\u0000\u0000\u0000"+ + "\u010a\u0108\u0001\u0000\u0000\u0000\u010b\u010c\u0006\n\uffff\uffff\u0000"+ + "\u010c\u0114\u0003D\"\u0000\u010d\u0114\u0003:\u001d\u0000\u010e\u0114"+ + "\u0003\u0016\u000b\u0000\u010f\u0110\u00050\u0000\u0000\u0110\u0111\u0003"+ + "\n\u0005\u0000\u0111\u0112\u00057\u0000\u0000\u0112\u0114\u0001\u0000"+ + "\u0000\u0000\u0113\u010b\u0001\u0000\u0000\u0000\u0113\u010d\u0001\u0000"+ + "\u0000\u0000\u0113\u010e\u0001\u0000\u0000\u0000\u0113\u010f\u0001\u0000"+ + "\u0000\u0000\u0114\u011a\u0001\u0000\u0000\u0000\u0115\u0116\n\u0001\u0000"+ + "\u0000\u0116\u0117\u0005%\u0000\u0000\u0117\u0119\u0003\u001a\r\u0000"+ + "\u0118\u0115\u0001\u0000\u0000\u0000\u0119\u011c\u0001\u0000\u0000\u0000"+ + "\u011a\u0118\u0001\u0000\u0000\u0000\u011a\u011b\u0001\u0000\u0000\u0000"+ + "\u011b\u0015\u0001\u0000\u0000\u0000\u011c\u011a\u0001\u0000\u0000\u0000"+ + "\u011d\u011e\u0003\u0018\f\u0000\u011e\u0128\u00050\u0000\u0000\u011f"+ + "\u0129\u0005B\u0000\u0000\u0120\u0125\u0003\n\u0005\u0000\u0121\u0122"+ + "\u0005\'\u0000\u0000\u0122\u0124\u0003\n\u0005\u0000\u0123\u0121\u0001"+ + "\u0000\u0000\u0000\u0124\u0127\u0001\u0000\u0000\u0000\u0125\u0123\u0001"+ + "\u0000\u0000\u0000\u0125\u0126\u0001\u0000\u0000\u0000\u0126\u0129\u0001"+ + "\u0000\u0000\u0000\u0127\u0125\u0001\u0000\u0000\u0000\u0128\u011f\u0001"+ + "\u0000\u0000\u0000\u0128\u0120\u0001\u0000\u0000\u0000\u0128\u0129\u0001"+ + "\u0000\u0000\u0000\u0129\u012a\u0001\u0000\u0000\u0000\u012a\u012b\u0005"+ + "7\u0000\u0000\u012b\u0017\u0001\u0000\u0000\u0000\u012c\u012d\u0003H$"+ + "\u0000\u012d\u0019\u0001\u0000\u0000\u0000\u012e\u012f\u0003@ \u0000\u012f"+ + "\u001b\u0001\u0000\u0000\u0000\u0130\u0131\u0005\f\u0000\u0000\u0131\u0132"+ + "\u0003\u001e\u000f\u0000\u0132\u001d\u0001\u0000\u0000\u0000\u0133\u0138"+ + "\u0003 \u0010\u0000\u0134\u0135\u0005\'\u0000\u0000\u0135\u0137\u0003"+ + " \u0010\u0000\u0136\u0134\u0001\u0000\u0000\u0000\u0137\u013a\u0001\u0000"+ + "\u0000\u0000\u0138\u0136\u0001\u0000\u0000\u0000\u0138\u0139\u0001\u0000"+ + "\u0000\u0000\u0139\u001f\u0001\u0000\u0000\u0000\u013a\u0138\u0001\u0000"+ + "\u0000\u0000\u013b\u013c\u0003:\u001d\u0000\u013c\u013d\u0005$\u0000\u0000"+ + "\u013d\u013f\u0001\u0000\u0000\u0000\u013e\u013b\u0001\u0000\u0000\u0000"+ + "\u013e\u013f\u0001\u0000\u0000\u0000\u013f\u0140\u0001\u0000\u0000\u0000"+ + "\u0140\u0141\u0003\n\u0005\u0000\u0141!\u0001\u0000\u0000\u0000\u0142"+ + "\u0143\u0005\u0006\u0000\u0000\u0143\u0148\u0003$\u0012\u0000\u0144\u0145"+ + "\u0005\'\u0000\u0000\u0145\u0147\u0003$\u0012\u0000\u0146\u0144\u0001"+ + "\u0000\u0000\u0000\u0147\u014a\u0001\u0000\u0000\u0000\u0148\u0146\u0001"+ + "\u0000\u0000\u0000\u0148\u0149\u0001\u0000\u0000\u0000\u0149\u014c\u0001"+ + "\u0000\u0000\u0000\u014a\u0148\u0001\u0000\u0000\u0000\u014b\u014d\u0003"+ + "*\u0015\u0000\u014c\u014b\u0001\u0000\u0000\u0000\u014c\u014d\u0001\u0000"+ + "\u0000\u0000\u014d#\u0001\u0000\u0000\u0000\u014e\u014f\u0003&\u0013\u0000"+ + "\u014f\u0150\u0005&\u0000\u0000\u0150\u0152\u0001\u0000\u0000\u0000\u0151"+ + "\u014e\u0001\u0000\u0000\u0000\u0151\u0152\u0001\u0000\u0000\u0000\u0152"+ + "\u0153\u0001\u0000\u0000\u0000\u0153\u0154\u0003(\u0014\u0000\u0154%\u0001"+ + "\u0000\u0000\u0000\u0155\u0156\u0005Q\u0000\u0000\u0156\'\u0001\u0000"+ + "\u0000\u0000\u0157\u0158\u0007\u0002\u0000\u0000\u0158)\u0001\u0000\u0000"+ + "\u0000\u0159\u015c\u0003,\u0016\u0000\u015a\u015c\u0003.\u0017\u0000\u015b"+ + "\u0159\u0001\u0000\u0000\u0000\u015b\u015a\u0001\u0000\u0000\u0000\u015c"+ + "+\u0001\u0000\u0000\u0000\u015d\u015e\u0005P\u0000\u0000\u015e\u0163\u0005"+ + "Q\u0000\u0000\u015f\u0160\u0005\'\u0000\u0000\u0160\u0162\u0005Q\u0000"+ + "\u0000\u0161\u015f\u0001\u0000\u0000\u0000\u0162\u0165\u0001\u0000\u0000"+ + "\u0000\u0163\u0161\u0001\u0000\u0000\u0000\u0163\u0164\u0001\u0000\u0000"+ + "\u0000\u0164-\u0001\u0000\u0000\u0000\u0165\u0163\u0001\u0000\u0000\u0000"+ + "\u0166\u0167\u0005F\u0000\u0000\u0167\u0168\u0003,\u0016\u0000\u0168\u0169"+ + "\u0005G\u0000\u0000\u0169/\u0001\u0000\u0000\u0000\u016a\u016b\u0005\u0013"+ + "\u0000\u0000\u016b\u0170\u0003$\u0012\u0000\u016c\u016d\u0005\'\u0000"+ + "\u0000\u016d\u016f\u0003$\u0012\u0000\u016e\u016c\u0001\u0000\u0000\u0000"+ + "\u016f\u0172\u0001\u0000\u0000\u0000\u0170\u016e\u0001\u0000\u0000\u0000"+ + "\u0170\u0171\u0001\u0000\u0000\u0000\u0171\u0174\u0001\u0000\u0000\u0000"+ + "\u0172\u0170\u0001\u0000\u0000\u0000\u0173\u0175\u00036\u001b\u0000\u0174"+ + "\u0173\u0001\u0000\u0000\u0000\u0174\u0175\u0001\u0000\u0000\u0000\u0175"+ + "\u0178\u0001\u0000\u0000\u0000\u0176\u0177\u0005!\u0000\u0000\u0177\u0179"+ + "\u0003\u001e\u000f\u0000\u0178\u0176\u0001\u0000\u0000\u0000\u0178\u0179"+ + "\u0001\u0000\u0000\u0000\u01791\u0001\u0000\u0000\u0000\u017a\u017b\u0005"+ + "\u0004\u0000\u0000\u017b\u017c\u0003\u001e\u000f\u0000\u017c3\u0001\u0000"+ + "\u0000\u0000\u017d\u017f\u0005\u000f\u0000\u0000\u017e\u0180\u00036\u001b"+ + "\u0000\u017f\u017e\u0001\u0000\u0000\u0000\u017f\u0180\u0001\u0000\u0000"+ + "\u0000\u0180\u0183\u0001\u0000\u0000\u0000\u0181\u0182\u0005!\u0000\u0000"+ + "\u0182\u0184\u0003\u001e\u000f\u0000\u0183\u0181\u0001\u0000\u0000\u0000"+ + "\u0183\u0184\u0001\u0000\u0000\u0000\u01845\u0001\u0000\u0000\u0000\u0185"+ + "\u018a\u00038\u001c\u0000\u0186\u0187\u0005\'\u0000\u0000\u0187\u0189"+ + "\u00038\u001c\u0000\u0188\u0186\u0001\u0000\u0000\u0000\u0189\u018c\u0001"+ + "\u0000\u0000\u0000\u018a\u0188\u0001\u0000\u0000\u0000\u018a\u018b\u0001"+ + "\u0000\u0000\u0000\u018b7\u0001\u0000\u0000\u0000\u018c\u018a\u0001\u0000"+ + "\u0000\u0000\u018d\u0190\u0003 \u0010\u0000\u018e\u018f\u0005\u0010\u0000"+ + "\u0000\u018f\u0191\u0003\n\u0005\u0000\u0190\u018e\u0001\u0000\u0000\u0000"+ + "\u0190\u0191\u0001\u0000\u0000\u0000\u01919\u0001\u0000\u0000\u0000\u0192"+ + "\u0197\u0003H$\u0000\u0193\u0194\u0005)\u0000\u0000\u0194\u0196\u0003"+ + "H$\u0000\u0195\u0193\u0001\u0000\u0000\u0000\u0196\u0199\u0001\u0000\u0000"+ + "\u0000\u0197\u0195\u0001\u0000\u0000\u0000\u0197\u0198\u0001\u0000\u0000"+ + "\u0000\u0198;\u0001\u0000\u0000\u0000\u0199\u0197\u0001\u0000\u0000\u0000"+ + "\u019a\u019f\u0003B!\u0000\u019b\u019c\u0005)\u0000\u0000\u019c\u019e"+ + "\u0003B!\u0000\u019d\u019b\u0001\u0000\u0000\u0000\u019e\u01a1\u0001\u0000"+ + "\u0000\u0000\u019f\u019d\u0001\u0000\u0000\u0000\u019f\u01a0\u0001\u0000"+ + "\u0000\u0000\u01a0=\u0001\u0000\u0000\u0000\u01a1\u019f\u0001\u0000\u0000"+ + "\u0000\u01a2\u01a7\u0003<\u001e\u0000\u01a3\u01a4\u0005\'\u0000\u0000"+ + "\u01a4\u01a6\u0003<\u001e\u0000\u01a5\u01a3\u0001\u0000\u0000\u0000\u01a6"+ + "\u01a9\u0001\u0000\u0000\u0000\u01a7\u01a5\u0001\u0000\u0000\u0000\u01a7"+ + "\u01a8\u0001\u0000\u0000\u0000\u01a8?\u0001\u0000\u0000\u0000\u01a9\u01a7"+ + "\u0001\u0000\u0000\u0000\u01aa\u01ab\u0007\u0003\u0000\u0000\u01abA\u0001"+ + "\u0000\u0000\u0000\u01ac\u01b0\u0005U\u0000\u0000\u01ad\u01ae\u0004!\n"+ + "\u0000\u01ae\u01b0\u0003F#\u0000\u01af\u01ac\u0001\u0000\u0000\u0000\u01af"+ + "\u01ad\u0001\u0000\u0000\u0000\u01b0C\u0001\u0000\u0000\u0000\u01b1\u01dc"+ + "\u00052\u0000\u0000\u01b2\u01b3\u0003h4\u0000\u01b3\u01b4\u0005H\u0000"+ + "\u0000\u01b4\u01dc\u0001\u0000\u0000\u0000\u01b5\u01dc\u0003f3\u0000\u01b6"+ + "\u01dc\u0003h4\u0000\u01b7\u01dc\u0003b1\u0000\u01b8\u01dc\u0003F#\u0000"+ + "\u01b9\u01dc\u0003j5\u0000\u01ba\u01bb\u0005F\u0000\u0000\u01bb\u01c0"+ + "\u0003d2\u0000\u01bc\u01bd\u0005\'\u0000\u0000\u01bd\u01bf\u0003d2\u0000"+ + "\u01be\u01bc\u0001\u0000\u0000\u0000\u01bf\u01c2\u0001\u0000\u0000\u0000"+ + "\u01c0\u01be\u0001\u0000\u0000\u0000\u01c0\u01c1\u0001\u0000\u0000\u0000"+ + "\u01c1\u01c3\u0001\u0000\u0000\u0000\u01c2\u01c0\u0001\u0000\u0000\u0000"+ + "\u01c3\u01c4\u0005G\u0000\u0000\u01c4\u01dc\u0001\u0000\u0000\u0000\u01c5"+ + "\u01c6\u0005F\u0000\u0000\u01c6\u01cb\u0003b1\u0000\u01c7\u01c8\u0005"+ + "\'\u0000\u0000\u01c8\u01ca\u0003b1\u0000\u01c9\u01c7\u0001\u0000\u0000"+ + "\u0000\u01ca\u01cd\u0001\u0000\u0000\u0000\u01cb\u01c9\u0001\u0000\u0000"+ + "\u0000\u01cb\u01cc\u0001\u0000\u0000\u0000\u01cc\u01ce\u0001\u0000\u0000"+ + "\u0000\u01cd\u01cb\u0001\u0000\u0000\u0000\u01ce\u01cf\u0005G\u0000\u0000"+ + "\u01cf\u01dc\u0001\u0000\u0000\u0000\u01d0\u01d1\u0005F\u0000\u0000\u01d1"+ + "\u01d6\u0003j5\u0000\u01d2\u01d3\u0005\'\u0000\u0000\u01d3\u01d5\u0003"+ + "j5\u0000\u01d4\u01d2\u0001\u0000\u0000\u0000\u01d5\u01d8\u0001\u0000\u0000"+ + "\u0000\u01d6\u01d4\u0001\u0000\u0000\u0000\u01d6\u01d7\u0001\u0000\u0000"+ + "\u0000\u01d7\u01d9\u0001\u0000\u0000\u0000\u01d8\u01d6\u0001\u0000\u0000"+ + "\u0000\u01d9\u01da\u0005G\u0000\u0000\u01da\u01dc\u0001\u0000\u0000\u0000"+ + "\u01db\u01b1\u0001\u0000\u0000\u0000\u01db\u01b2\u0001\u0000\u0000\u0000"+ + "\u01db\u01b5\u0001\u0000\u0000\u0000\u01db\u01b6\u0001\u0000\u0000\u0000"+ + "\u01db\u01b7\u0001\u0000\u0000\u0000\u01db\u01b8\u0001\u0000\u0000\u0000"+ + "\u01db\u01b9\u0001\u0000\u0000\u0000\u01db\u01ba\u0001\u0000\u0000\u0000"+ + "\u01db\u01c5\u0001\u0000\u0000\u0000\u01db\u01d0\u0001\u0000\u0000\u0000"+ + "\u01dcE\u0001\u0000\u0000\u0000\u01dd\u01e0\u00055\u0000\u0000\u01de\u01e0"+ + "\u0005E\u0000\u0000\u01df\u01dd\u0001\u0000\u0000\u0000\u01df\u01de\u0001"+ + "\u0000\u0000\u0000\u01e0G\u0001\u0000\u0000\u0000\u01e1\u01e5\u0003@ "+ + "\u0000\u01e2\u01e3\u0004$\u000b\u0000\u01e3\u01e5\u0003F#\u0000\u01e4"+ + "\u01e1\u0001\u0000\u0000\u0000\u01e4\u01e2\u0001\u0000\u0000\u0000\u01e5"+ + "I\u0001\u0000\u0000\u0000\u01e6\u01e7\u0005\t\u0000\u0000\u01e7\u01e8"+ + "\u0005\u001f\u0000\u0000\u01e8K\u0001\u0000\u0000\u0000\u01e9\u01ea\u0005"+ + "\u000e\u0000\u0000\u01ea\u01ef\u0003N\'\u0000\u01eb\u01ec\u0005\'\u0000"+ + "\u0000\u01ec\u01ee\u0003N\'\u0000\u01ed\u01eb\u0001\u0000\u0000\u0000"+ + "\u01ee\u01f1\u0001\u0000\u0000\u0000\u01ef\u01ed\u0001\u0000\u0000\u0000"+ + "\u01ef\u01f0\u0001\u0000\u0000\u0000\u01f0M\u0001\u0000\u0000\u0000\u01f1"+ + "\u01ef\u0001\u0000\u0000\u0000\u01f2\u01f4\u0003\n\u0005\u0000\u01f3\u01f5"+ + "\u0007\u0004\u0000\u0000\u01f4\u01f3\u0001\u0000\u0000\u0000\u01f4\u01f5"+ + "\u0001\u0000\u0000\u0000\u01f5\u01f8\u0001\u0000\u0000\u0000\u01f6\u01f7"+ + "\u00053\u0000\u0000\u01f7\u01f9\u0007\u0005\u0000\u0000\u01f8\u01f6\u0001"+ + "\u0000\u0000\u0000\u01f8\u01f9\u0001\u0000\u0000\u0000\u01f9O\u0001\u0000"+ + "\u0000\u0000\u01fa\u01fb\u0005\b\u0000\u0000\u01fb\u01fc\u0003>\u001f"+ + "\u0000\u01fcQ\u0001\u0000\u0000\u0000\u01fd\u01fe\u0005\u0002\u0000\u0000"+ + "\u01fe\u01ff\u0003>\u001f\u0000\u01ffS\u0001\u0000\u0000\u0000\u0200\u0201"+ + "\u0005\u000b\u0000\u0000\u0201\u0206\u0003V+\u0000\u0202\u0203\u0005\'"+ + "\u0000\u0000\u0203\u0205\u0003V+\u0000\u0204\u0202\u0001\u0000\u0000\u0000"+ + "\u0205\u0208\u0001\u0000\u0000\u0000\u0206\u0204\u0001\u0000\u0000\u0000"+ + "\u0206\u0207\u0001\u0000\u0000\u0000\u0207U\u0001\u0000\u0000\u0000\u0208"+ + "\u0206\u0001\u0000\u0000\u0000\u0209\u020a\u0003<\u001e\u0000\u020a\u020b"+ + "\u0005Y\u0000\u0000\u020b\u020c\u0003<\u001e\u0000\u020cW\u0001\u0000"+ + "\u0000\u0000\u020d\u020e\u0005\u0001\u0000\u0000\u020e\u020f\u0003\u0014"+ + "\n\u0000\u020f\u0211\u0003j5\u0000\u0210\u0212\u0003^/\u0000\u0211\u0210"+ + "\u0001\u0000\u0000\u0000\u0211\u0212\u0001\u0000\u0000\u0000\u0212Y\u0001"+ + "\u0000\u0000\u0000\u0213\u0214\u0005\u0007\u0000\u0000\u0214\u0215\u0003"+ + "\u0014\n\u0000\u0215\u0216\u0003j5\u0000\u0216[\u0001\u0000\u0000\u0000"+ + "\u0217\u0218\u0005\n\u0000\u0000\u0218\u0219\u0003:\u001d\u0000\u0219"+ + "]\u0001\u0000\u0000\u0000\u021a\u021f\u0003`0\u0000\u021b\u021c\u0005"+ + "\'\u0000\u0000\u021c\u021e\u0003`0\u0000\u021d\u021b\u0001\u0000\u0000"+ + "\u0000\u021e\u0221\u0001\u0000\u0000\u0000\u021f\u021d\u0001\u0000\u0000"+ + "\u0000\u021f\u0220\u0001\u0000\u0000\u0000\u0220_\u0001\u0000\u0000\u0000"+ + "\u0221\u021f\u0001\u0000\u0000\u0000\u0222\u0223\u0003@ \u0000\u0223\u0224"+ + "\u0005$\u0000\u0000\u0224\u0225\u0003D\"\u0000\u0225a\u0001\u0000\u0000"+ + "\u0000\u0226\u0227\u0007\u0006\u0000\u0000\u0227c\u0001\u0000\u0000\u0000"+ + "\u0228\u022b\u0003f3\u0000\u0229\u022b\u0003h4\u0000\u022a\u0228\u0001"+ + "\u0000\u0000\u0000\u022a\u0229\u0001\u0000\u0000\u0000\u022be\u0001\u0000"+ + "\u0000\u0000\u022c\u022e\u0007\u0000\u0000\u0000\u022d\u022c\u0001\u0000"+ + "\u0000\u0000\u022d\u022e\u0001\u0000\u0000\u0000\u022e\u022f\u0001\u0000"+ + "\u0000\u0000\u022f\u0230\u0005 \u0000\u0000\u0230g\u0001\u0000\u0000\u0000"+ + "\u0231\u0233\u0007\u0000\u0000\u0000\u0232\u0231\u0001\u0000\u0000\u0000"+ + "\u0232\u0233\u0001\u0000\u0000\u0000\u0233\u0234\u0001\u0000\u0000\u0000"+ + "\u0234\u0235\u0005\u001f\u0000\u0000\u0235i\u0001\u0000\u0000\u0000\u0236"+ + "\u0237\u0005\u001e\u0000\u0000\u0237k\u0001\u0000\u0000\u0000\u0238\u0239"+ + "\u0007\u0007\u0000\u0000\u0239m\u0001\u0000\u0000\u0000\u023a\u023b\u0005"+ + "\u0005\u0000\u0000\u023b\u023c\u0003p8\u0000\u023co\u0001\u0000\u0000"+ + "\u0000\u023d\u023e\u0005F\u0000\u0000\u023e\u023f\u0003\u0002\u0001\u0000"+ + "\u023f\u0240\u0005G\u0000\u0000\u0240q\u0001\u0000\u0000\u0000\u0241\u0242"+ + "\u0005\r\u0000\u0000\u0242\u0243\u0005i\u0000\u0000\u0243s\u0001\u0000"+ + "\u0000\u0000\u0244\u0245\u0005\u0003\u0000\u0000\u0245\u0248\u0005_\u0000"+ + "\u0000\u0246\u0247\u0005]\u0000\u0000\u0247\u0249\u0003<\u001e\u0000\u0248"+ + "\u0246\u0001\u0000\u0000\u0000\u0248\u0249\u0001\u0000\u0000\u0000\u0249"+ + "\u0253\u0001\u0000\u0000\u0000\u024a\u024b\u0005^\u0000\u0000\u024b\u0250"+ + "\u0003v;\u0000\u024c\u024d\u0005\'\u0000\u0000\u024d\u024f\u0003v;\u0000"+ + "\u024e\u024c\u0001\u0000\u0000\u0000\u024f\u0252\u0001\u0000\u0000\u0000"+ + "\u0250\u024e\u0001\u0000\u0000\u0000\u0250\u0251\u0001\u0000\u0000\u0000"+ + "\u0251\u0254\u0001\u0000\u0000\u0000\u0252\u0250\u0001\u0000\u0000\u0000"+ + "\u0253\u024a\u0001\u0000\u0000\u0000\u0253\u0254\u0001\u0000\u0000\u0000"+ + "\u0254u\u0001\u0000\u0000\u0000\u0255\u0256\u0003<\u001e\u0000\u0256\u0257"+ + "\u0005$\u0000\u0000\u0257\u0259\u0001\u0000\u0000\u0000\u0258\u0255\u0001"+ + "\u0000\u0000\u0000\u0258\u0259\u0001\u0000\u0000\u0000\u0259\u025a\u0001"+ + "\u0000\u0000\u0000\u025a\u025b\u0003<\u001e\u0000\u025bw\u0001\u0000\u0000"+ + "\u0000\u025c\u025d\u0005\u0012\u0000\u0000\u025d\u025e\u0003$\u0012\u0000"+ + "\u025e\u025f\u0005]\u0000\u0000\u025f\u0260\u0003>\u001f\u0000\u0260y"+ + "\u0001\u0000\u0000\u0000\u0261\u0262\u0005\u0011\u0000\u0000\u0262\u0265"+ + "\u00036\u001b\u0000\u0263\u0264\u0005!\u0000\u0000\u0264\u0266\u0003\u001e"+ + "\u000f\u0000\u0265\u0263\u0001\u0000\u0000\u0000\u0265\u0266\u0001\u0000"+ + "\u0000\u0000\u0266{\u0001\u0000\u0000\u0000\u0267\u0269\u0007\b\u0000"+ + "\u0000\u0268\u0267\u0001\u0000\u0000\u0000\u0268\u0269\u0001\u0000\u0000"+ + "\u0000\u0269\u026a\u0001\u0000\u0000\u0000\u026a\u026b\u0005\u0014\u0000"+ + "\u0000\u026b\u026c\u0003~?\u0000\u026c\u026d\u0003\u0080@\u0000\u026d"+ + "}\u0001\u0000\u0000\u0000\u026e\u0271\u0003@ \u0000\u026f\u0270\u0005"+ + "Y\u0000\u0000\u0270\u0272\u0003@ \u0000\u0271\u026f\u0001\u0000\u0000"+ + "\u0000\u0271\u0272\u0001\u0000\u0000\u0000\u0272\u007f\u0001\u0000\u0000"+ + "\u0000\u0273\u0274\u0005]\u0000\u0000\u0274\u0279\u0003\u0082A\u0000\u0275"+ + "\u0276\u0005\'\u0000\u0000\u0276\u0278\u0003\u0082A\u0000\u0277\u0275"+ + "\u0001\u0000\u0000\u0000\u0278\u027b\u0001\u0000\u0000\u0000\u0279\u0277"+ + "\u0001\u0000\u0000\u0000\u0279\u027a\u0001\u0000\u0000\u0000\u027a\u0081"+ + "\u0001\u0000\u0000\u0000\u027b\u0279\u0001\u0000\u0000\u0000\u027c\u027d"+ + "\u0003\u0010\b\u0000\u027d\u0083\u0001\u0000\u0000\u0000>\u008f\u0098"+ + "\u00ac\u00b8\u00c1\u00c9\u00ce\u00d6\u00d8\u00dd\u00e4\u00e9\u00ee\u00f8"+ + "\u00fe\u0106\u0108\u0113\u011a\u0125\u0128\u0138\u013e\u0148\u014c\u0151"+ + "\u015b\u0163\u0170\u0174\u0178\u017f\u0183\u018a\u0190\u0197\u019f\u01a7"+ + "\u01af\u01c0\u01cb\u01d6\u01db\u01df\u01e4\u01ef\u01f4\u01f8\u0206\u0211"+ + "\u021f\u022a\u022d\u0232\u0248\u0250\u0253\u0258\u0265\u0268\u0271\u0279"; public static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); static { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java index c428a2c6411a..81d43bc68b79 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java @@ -627,13 +627,16 @@ public String visitIdentifierOrParameter(EsqlBaseParser.IdentifierOrParameterCon @Override public Expression visitInlineCast(EsqlBaseParser.InlineCastContext ctx) { - Source source = source(ctx); - DataType dataType = typedParsing(this, ctx.dataType(), DataType.class); + return castToType(source(ctx), ctx.primaryExpression(), ctx.dataType()); + } + + private Expression castToType(Source source, ParseTree parseTree, EsqlBaseParser.DataTypeContext dataTypeCtx) { + DataType dataType = typedParsing(this, dataTypeCtx, DataType.class); var converterToFactory = EsqlDataTypeConverter.converterFunctionFactory(dataType); if (converterToFactory == null) { throw new ParsingException(source, "Unsupported conversion to type [{}]", dataType); } - Expression expr = expression(ctx.primaryExpression()); + Expression expr = expression(parseTree); return converterToFactory.apply(source, expr); } @@ -923,6 +926,14 @@ String unresolvedAttributeNameInParam(ParserRuleContext ctx, Expression param) { @Override public Expression visitMatchBooleanExpression(EsqlBaseParser.MatchBooleanExpressionContext ctx) { - return new Match(source(ctx), expression(ctx.fieldExp), expression(ctx.queryString)); + + final Expression matchFieldExpression; + if (ctx.fieldType != null) { + matchFieldExpression = castToType(source(ctx), ctx.fieldExp, ctx.fieldType); + } else { + matchFieldExpression = expression(ctx.fieldExp); + } + + return new Match(source(ctx), matchFieldExpression, expression(ctx.matchQuery)); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java index 1aee8f029e47..7820f0f657f7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java @@ -33,11 +33,13 @@ import org.elasticsearch.xpack.esql.core.querydsl.query.TermsQuery; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField; import org.elasticsearch.xpack.esql.core.util.Check; import org.elasticsearch.xpack.esql.expression.function.fulltext.Kql; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString; import org.elasticsearch.xpack.esql.expression.function.fulltext.Term; +import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils; @@ -533,28 +535,43 @@ private static RangeQuery translate(Range r, TranslatorHandler handler) { public static class MatchFunctionTranslator extends ExpressionTranslator { @Override protected Query asQuery(Match match, TranslatorHandler handler) { - return new MatchQuery(match.source(), ((FieldAttribute) match.field()).name(), match.queryAsText()); + Expression fieldExpression = match.field(); + // Field may be converted to other data type (field_name :: data_type), so we need to check the original field + if (fieldExpression instanceof AbstractConvertFunction convertFunction) { + fieldExpression = convertFunction.field(); + } + if (fieldExpression instanceof FieldAttribute fieldAttribute) { + String fieldName = fieldAttribute.name(); + if (fieldAttribute.field() instanceof MultiTypeEsField multiTypeEsField) { + // If we have multiple field types, we allow the query to be done, but getting the underlying field name + fieldName = multiTypeEsField.getName(); + } + // Make query lenient so mixed field types can be queried when a field type is incompatible with the value provided + return new MatchQuery(match.source(), fieldName, match.queryAsObject(), Map.of("lenient", "true")); + } + + throw new IllegalArgumentException("Match must have a field attribute as the first argument"); } } public static class QueryStringFunctionTranslator extends ExpressionTranslator { @Override protected Query asQuery(QueryString queryString, TranslatorHandler handler) { - return new QueryStringQuery(queryString.source(), queryString.queryAsText(), Map.of(), Map.of()); + return new QueryStringQuery(queryString.source(), (String) queryString.queryAsObject(), Map.of(), Map.of()); } } public static class KqlFunctionTranslator extends ExpressionTranslator { @Override protected Query asQuery(Kql kqlFunction, TranslatorHandler handler) { - return new KqlQuery(kqlFunction.source(), kqlFunction.queryAsText()); + return new KqlQuery(kqlFunction.source(), (String) kqlFunction.queryAsObject()); } } public static class TermFunctionTranslator extends ExpressionTranslator { @Override protected Query asQuery(Term term, TranslatorHandler handler) { - return new TermQuery(term.source(), ((FieldAttribute) term.field()).name(), term.queryAsText()); + return new TermQuery(term.source(), ((FieldAttribute) term.field()).name(), term.queryAsObject()); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 6edbb55af463..30aec707df54 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -261,10 +261,23 @@ public void testProjectIncludeMultiStarPattern() { } public void testProjectStar() { - assertProjection(""" - from test - | keep * - """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary"); + assertProjection( + """ + from test + | keep * + """, + "_meta_field", + "emp_no", + "first_name", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "last_name", + "long_noidx", + "salary" + ); } public void testEscapedStar() { @@ -297,9 +310,22 @@ public void testRenameBacktickPlusPattern() { } public void testNoProjection() { - assertProjection(""" - from test - """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary"); + assertProjection( + """ + from test + """, + "_meta_field", + "emp_no", + "first_name", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "last_name", + "long_noidx", + "salary" + ); assertProjectionTypes( """ from test @@ -308,6 +334,7 @@ public void testNoProjection() { DataType.INTEGER, DataType.KEYWORD, DataType.TEXT, + DataType.DATETIME, DataType.TEXT, DataType.KEYWORD, DataType.INTEGER, @@ -329,18 +356,57 @@ public void testDuplicateProjections() { } public void testProjectWildcard() { - assertProjection(""" - from test - | keep first_name, *, last_name - """, "first_name", "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "last_name"); - assertProjection(""" - from test - | keep first_name, last_name, * - """, "first_name", "last_name", "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary"); - assertProjection(""" - from test - | keep *, first_name, last_name - """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "first_name", "last_name"); + assertProjection( + """ + from test + | keep first_name, *, last_name + """, + "first_name", + "_meta_field", + "emp_no", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary", + "last_name" + ); + assertProjection( + """ + from test + | keep first_name, last_name, * + """, + "first_name", + "last_name", + "_meta_field", + "emp_no", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary" + ); + assertProjection( + """ + from test + | keep *, first_name, last_name + """, + "_meta_field", + "emp_no", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary", + "first_name", + "last_name" + ); var e = expectThrows(ParsingException.class, () -> analyze(""" from test @@ -363,22 +429,74 @@ public void testProjectMixedWildcard() { from test | keep *ob*, first_name, *name, first* """, "job", "job.raw", "first_name", "last_name"); - assertProjection(""" - from test - | keep first_name, *, *name - """, "first_name", "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "last_name"); - assertProjection(""" - from test - | keep first*, *, last_name, first_name - """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "last_name", "first_name"); - assertProjection(""" - from test - | keep first*, *, last_name, fir* - """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "last_name", "first_name"); - assertProjection(""" - from test - | keep *, job* - """, "_meta_field", "emp_no", "first_name", "gender", "languages", "last_name", "long_noidx", "salary", "job", "job.raw"); + assertProjection( + """ + from test + | keep first_name, *, *name + """, + "first_name", + "_meta_field", + "emp_no", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary", + "last_name" + ); + assertProjection( + """ + from test + | keep first*, *, last_name, first_name + """, + "_meta_field", + "emp_no", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary", + "last_name", + "first_name" + ); + assertProjection( + """ + from test + | keep first*, *, last_name, fir* + """, + "_meta_field", + "emp_no", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary", + "last_name", + "first_name" + ); + assertProjection( + """ + from test + | keep *, job* + """, + "_meta_field", + "emp_no", + "first_name", + "gender", + "hire_date", + "languages", + "last_name", + "long_noidx", + "salary", + "job", + "job.raw" + ); } public void testProjectThenDropName() { @@ -410,21 +528,34 @@ public void testProjectDropPattern() { from test | keep * | drop *_name - """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary"); + """, "_meta_field", "emp_no", "gender", "hire_date", "job", "job.raw", "languages", "long_noidx", "salary"); } public void testProjectDropNoStarPattern() { assertProjection(""" from test | drop *_name - """, "_meta_field", "emp_no", "gender", "job", "job.raw", "languages", "long_noidx", "salary"); + """, "_meta_field", "emp_no", "gender", "hire_date", "job", "job.raw", "languages", "long_noidx", "salary"); } public void testProjectOrderPatternWithRest() { - assertProjection(""" - from test - | keep *name, *, emp_no - """, "first_name", "last_name", "_meta_field", "gender", "job", "job.raw", "languages", "long_noidx", "salary", "emp_no"); + assertProjection( + """ + from test + | keep *name, *, emp_no + """, + "first_name", + "last_name", + "_meta_field", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "long_noidx", + "salary", + "emp_no" + ); } public void testProjectDropPatternAndKeepOthers() { @@ -563,7 +694,7 @@ public void testDropPatternUnsupportedFields() { assertProjection(""" from test | drop *ala* - """, "_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx"); + """, "_meta_field", "emp_no", "first_name", "gender", "hire_date", "job", "job.raw", "languages", "last_name", "long_noidx"); } public void testDropUnsupportedPattern() { @@ -633,7 +764,7 @@ public void testRenameReuseAlias() { assertProjection(""" from test | rename emp_no as e, first_name as e - """, "_meta_field", "e", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary"); + """, "_meta_field", "e", "gender", "hire_date", "job", "job.raw", "languages", "last_name", "long_noidx", "salary"); } public void testRenameUnsupportedSubFieldAndResolved() { @@ -1946,6 +2077,7 @@ public void testLookup() { .item(startsWith("emp_no{f}")) .item(startsWith("first_name{f}")) .item(startsWith("gender{f}")) + .item(startsWith("hire_date{f}")) .item(startsWith("job{f}")) .item(startsWith("job.raw{f}")) /* diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 7e3ef4f1f5f8..92cac30f1bb2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1166,11 +1166,6 @@ public void testMatchInsideEval() throws Exception { } public void testMatchFilter() throws Exception { - assertEquals( - "1:19: first argument of [salary:\"100\"] must be [string], found value [salary] type [integer]", - error("from test | where salary:\"100\"") - ); - assertEquals( "1:19: Invalid condition [first_name:\"Anna\" or starts_with(first_name, \"Anne\")]. " + "[:] operator can't be used as part of an or condition", diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java index da92eba1e4a0..c609eb3a7ad4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java @@ -739,7 +739,10 @@ public static void testFunctionInfo() { } log.info("{}: tested {} vs annotated {}", arg.name(), signatureTypes, annotationTypes); assertEquals( - "Missmatch between actual and declared parameter types. You probably need to update your @params annotations.", + "Mismatch between actual and declared param type for [" + + arg.name() + + "]. " + + "You probably need to update your @params annotations or add test cases to your test.", signatureTypes, annotationTypes ); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java index 32e9670286ef..951aff80541b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java @@ -10,16 +10,13 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.FunctionName; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import java.util.LinkedList; -import java.util.List; import java.util.function.Supplier; /** - * This class is only used to generates docs for the match operator - all testing is done in {@link MatchTests} + * This class is only used to generates docs for the match operator - all testing is the same as {@link MatchTests} */ @FunctionName("match_operator") public class MatchOperatorTests extends MatchTests { @@ -30,12 +27,6 @@ public MatchOperatorTests(@Name("TestCase") Supplier @ParametersFactory public static Iterable parameters() { - // Have a minimal test so that we can generate the appropriate types in the docs - List suppliers = new LinkedList<>(); - addPositiveTestCase(List.of(DataType.KEYWORD, DataType.KEYWORD), suppliers); - addPositiveTestCase(List.of(DataType.TEXT, DataType.TEXT), suppliers); - addPositiveTestCase(List.of(DataType.KEYWORD, DataType.TEXT), suppliers); - addPositiveTestCase(List.of(DataType.TEXT, DataType.KEYWORD), suppliers); - return parameterSuppliersFromTypedData(suppliers); + return MatchTests.parameters(); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java index 6a4a7404135f..f29add60721d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java @@ -10,119 +10,411 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import org.apache.lucene.util.BytesRef; -import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression; import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.FunctionName; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; +import java.math.BigInteger; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.stringCases; import static org.hamcrest.Matchers.equalTo; @FunctionName("match") public class MatchTests extends AbstractFunctionTestCase { + private static final String FIELD_TYPE_ERROR_STRING = + "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version"; + + private static final String QUERY_TYPE_ERROR_STRING = + "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version"; + public MatchTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @ParametersFactory public static Iterable parameters() { - List> supportedPerPosition = supportedParams(); - List suppliers = new LinkedList<>(); - for (DataType fieldType : DataType.stringTypes()) { - for (DataType queryType : DataType.stringTypes()) { - addPositiveTestCase(List.of(fieldType, queryType), suppliers); - addNonFieldTestCase(List.of(fieldType, queryType), supportedPerPosition, suppliers); - } - } + List suppliers = new ArrayList<>(); - List suppliersWithErrors = errorsForCasesWithoutExamples(suppliers, (v, p) -> "string"); + addUnsignedLongCases(suppliers); + addNumericCases(suppliers); + addNonNumericCases(suppliers); + addQueryAsStringTestCases(suppliers); + addStringTestCases(suppliers); - // Don't test null, as it is not allowed but the expected message is not a type error - so we check it separately in VerifierTests return parameterSuppliersFromTypedData( - suppliersWithErrors.stream().filter(s -> s.types().contains(DataType.NULL) == false).toList() + errorsForCasesWithoutExamples( + suppliers, + (o, v, t) -> errorMessageStringForMatch(o, v, t, (l, p) -> p == 0 ? FIELD_TYPE_ERROR_STRING : QUERY_TYPE_ERROR_STRING) + ) ); } - protected static List> supportedParams() { - Set supportedTextParams = Set.of(DataType.KEYWORD, DataType.TEXT); - Set supportedNumericParams = Set.of(DataType.DOUBLE, DataType.INTEGER); - Set supportedFuzzinessParams = Set.of(DataType.INTEGER, DataType.KEYWORD, DataType.TEXT); - List> supportedPerPosition = List.of( - supportedTextParams, - supportedTextParams, - supportedNumericParams, - supportedFuzzinessParams - ); - return supportedPerPosition; + private static String errorMessageStringForMatch( + boolean includeOrdinal, + List> validPerPosition, + List types, + PositionalErrorMessageSupplier positionalErrorMessageSupplier + ) { + for (int i = 0; i < types.size(); i++) { + // Need to check for nulls and bad parameters in order + if (types.get(i) == DataType.NULL) { + return TypeResolutions.ParamOrdinal.fromIndex(i).name().toLowerCase(Locale.ROOT) + + " argument of [] cannot be null, received [null]"; + } + if (validPerPosition.get(i).contains(types.get(i)) == false) { + break; + } + } + + try { + return typeErrorMessage(includeOrdinal, validPerPosition, types, positionalErrorMessageSupplier); + } catch (IllegalStateException e) { + // This means all the positional args were okay, so the expected error is for nulls or from the combination + return EsqlBinaryComparison.formatIncompatibleTypesMessage(types.get(0), types.get(1), ""); + } } - protected static void addPositiveTestCase(List paramDataTypes, List suppliers) { + private static void addNonNumericCases(List suppliers) { + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.booleanCases(), + TestCaseSupplier.booleanCases(), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ipCases(), + TestCaseSupplier.ipCases(), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.versionCases(""), + TestCaseSupplier.versionCases(""), + List.of(), + false + ) + ); + // Datetime + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateCases(), + TestCaseSupplier.dateCases(), + List.of(), + false + ) + ); - // Positive case - creates an ES field from the field parameter type - suppliers.add( - new TestCaseSupplier( - getTestCaseName(paramDataTypes, "-ES field"), - paramDataTypes, - () -> new TestCaseSupplier.TestCase( - getTestParams(paramDataTypes), - "EndsWithEvaluator[str=Attribute[channel=0], suffix=Attribute[channel=1]]", - DataType.BOOLEAN, - equalTo(true) - ) + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateNanosCases(), + TestCaseSupplier.dateNanosCases(), + List.of(), + false ) ); } - private static void addNonFieldTestCase( - List paramDataTypes, - List> supportedPerPosition, - List suppliers - ) { - // Negative case - use directly the field parameter type - suppliers.add( - new TestCaseSupplier( - getTestCaseName(paramDataTypes, "-non ES field"), - paramDataTypes, - typeErrorSupplier(true, supportedPerPosition, paramDataTypes, MatchTests::matchTypeErrorSupplier) + private static void addNumericCases(List suppliers) { + suppliers.addAll( + TestCaseSupplier.forBinaryComparisonWithWidening( + new TestCaseSupplier.NumericTypeTestConfigs<>( + new TestCaseSupplier.NumericTypeTestConfig<>( + (Integer.MIN_VALUE >> 1) - 1, + (Integer.MAX_VALUE >> 1) - 1, + (l, r) -> true, + "EqualsIntsEvaluator" + ), + new TestCaseSupplier.NumericTypeTestConfig<>( + (Long.MIN_VALUE >> 1) - 1, + (Long.MAX_VALUE >> 1) - 1, + (l, r) -> true, + "EqualsLongsEvaluator" + ), + new TestCaseSupplier.NumericTypeTestConfig<>( + Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY, + // NB: this has different behavior than Double::equals + (l, r) -> true, + "EqualsDoublesEvaluator" + ) + ), + "field", + "query", + (lhs, rhs) -> List.of(), + false ) ); } - private static List getTestParams(List paramDataTypes) { - String fieldName = randomIdentifier(); - List params = new ArrayList<>(); - params.add( - new TestCaseSupplier.TypedData( - new FieldExpression(fieldName, List.of(new FieldExpression.FieldValue(fieldName))), - paramDataTypes.get(0), - "field" + private static void addUnsignedLongCases(List suppliers) { + // TODO: These should be integrated into the type cross product above, but are currently broken + // see https://github.com/elastic/elasticsearch/issues/102935 + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.longCases(Long.MIN_VALUE, Long.MAX_VALUE, true), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.doubleCases(Double.MIN_VALUE, Double.MAX_VALUE, true), + List.of(), + false ) ); - params.add(new TestCaseSupplier.TypedData(new BytesRef(randomAlphaOfLength(10)), paramDataTypes.get(1), "query")); - return params; } - private static String getTestCaseName(List paramDataTypes, String fieldType) { - StringBuilder sb = new StringBuilder(); - sb.append("<"); - sb.append(paramDataTypes.get(0)).append(fieldType).append(", "); - sb.append(paramDataTypes.get(1)); - sb.append(">"); - return sb.toString(); + private static void addQueryAsStringTestCases(List suppliers) { + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.longCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.doubleCases(Double.MIN_VALUE, Double.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + // Unsigned Long cases + // TODO: These should be integrated into the type cross product above, but are currently broken + // see https://github.com/elastic/elasticsearch/issues/102935 + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.booleanCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ipCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.versionCases(""), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + // Datetime + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateNanosCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); } - private static String matchTypeErrorSupplier(boolean includeOrdinal, List> validPerPosition, List types) { - return "[] cannot operate on [" + types.getFirst().typeName() + "], which is not a field from an index mapping"; + private static void addStringTestCases(List suppliers) { + for (DataType fieldType : DataType.stringTypes()) { + if (DataType.UNDER_CONSTRUCTION.containsKey(fieldType)) { + continue; + } + for (TestCaseSupplier.TypedDataSupplier queryDataSupplier : stringCases(fieldType)) { + suppliers.add( + TestCaseSupplier.testCaseSupplier( + queryDataSupplier, + new TestCaseSupplier.TypedDataSupplier(fieldType.typeName(), () -> randomAlphaOfLength(10), DataType.KEYWORD), + (d1, d2) -> equalTo("string"), + DataType.BOOLEAN, + (o1, o2) -> true + ) + ); + } + } + } + + public final void testLiteralExpressions() { + Expression expression = buildLiteralExpression(testCase); + if (testCase.getExpectedTypeError() != null) { + assertTypeResolutionFailure(expression); + return; + } + assertFalse("expected resolved", expression.typeResolved().unresolved()); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java index baef20081a4f..0c03556241d2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java @@ -372,6 +372,7 @@ public void testMissingFieldInFilterNoProjection() { "emp_no", "first_name", "gender", + "hire_date", "job", "job.raw", "languages", diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index d32124c1aaf3..6123a464378f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -10,6 +10,7 @@ import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.lucene.search.IndexSearcher; +import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexMode; @@ -23,6 +24,7 @@ import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.enrich.EnrichPolicy; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.EsqlTestUtils.TestSearchStats; @@ -40,9 +42,11 @@ import org.elasticsearch.xpack.esql.core.util.Holder; import org.elasticsearch.xpack.esql.enrich.ResolvedEnrichPolicy; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; +import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; import org.elasticsearch.xpack.esql.optimizer.rules.logical.ExtractAggregateCommonFilter; +import org.elasticsearch.xpack.esql.parser.ParsingException; import org.elasticsearch.xpack.esql.plan.logical.Enrich; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.plan.physical.AggregateExec; @@ -66,14 +70,17 @@ import org.elasticsearch.xpack.esql.stats.Metrics; import org.elasticsearch.xpack.esql.stats.SearchContextStats; import org.elasticsearch.xpack.esql.stats.SearchStats; +import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; import org.elasticsearch.xpack.kql.query.KqlQueryBuilder; import org.junit.Before; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Function; import static java.util.Arrays.asList; @@ -93,12 +100,22 @@ //@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE,org.elasticsearch.compute:TRACE", reason = "debug") public class LocalPhysicalPlanOptimizerTests extends MapperServiceTestCase { + public static final List UNNECESSARY_CASTING_DATA_TYPES = List.of( + DataType.BOOLEAN, + DataType.INTEGER, + DataType.LONG, + DataType.DOUBLE, + DataType.KEYWORD, + DataType.TEXT + ); private static final String PARAM_FORMATTING = "%1$s"; /** * Estimated size of a keyword field in bytes. */ private static final int KEYWORD_EST = EstimatesRowSize.estimateSize(DataType.KEYWORD); + public static final String MATCH_OPERATOR_QUERY = "from test | where %s:%s"; + public static final String MATCH_FUNCTION_QUERY = "from test | where match(%s, %s)"; private TestPlannerOptimizer plannerOptimizer; private final Configuration config; @@ -629,7 +646,7 @@ public void testMatchFunction() { var field = as(project.child(), FieldExtractExec.class); var query = as(field.child(), EsQueryExec.class); assertThat(query.limit().fold(), is(1000)); - var expected = QueryBuilders.matchQuery("last_name", "Smith"); + var expected = QueryBuilders.matchQuery("last_name", "Smith").lenient(true); assertThat(query.query().toString(), is(expected.toString())); } @@ -661,7 +678,7 @@ public void testMatchFunctionConjunctionWhereOperands() { Source filterSource = new Source(2, 38, "emp_no > 10000"); var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource); - var queryString = QueryBuilders.matchQuery("last_name", "Smith"); + var queryString = QueryBuilders.matchQuery("last_name", "Smith").lenient(true); var expected = QueryBuilders.boolQuery().must(queryString).must(range); assertThat(query.query().toString(), is(expected.toString())); } @@ -696,7 +713,7 @@ public void testMatchFunctionWithFunctionsPushedToLucene() { Source filterSource = new Source(2, 32, "cidr_match(ip, \"127.0.0.1/32\")"); var terms = wrapWithSingleQuery(queryText, QueryBuilders.termsQuery("ip", "127.0.0.1/32"), "ip", filterSource); - var queryString = QueryBuilders.matchQuery("text", "beta"); + var queryString = QueryBuilders.matchQuery("text", "beta").lenient(true); var expected = QueryBuilders.boolQuery().must(queryString).must(terms); assertThat(query.query().toString(), is(expected.toString())); } @@ -730,7 +747,7 @@ public void testMatchFunctionMultipleWhereClauses() { Source filterSource = new Source(3, 8, "emp_no > 10000"); var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource); - var queryString = QueryBuilders.matchQuery("last_name", "Smith"); + var queryString = QueryBuilders.matchQuery("last_name", "Smith").lenient(true); var expected = QueryBuilders.boolQuery().must(queryString).must(range); assertThat(query.query().toString(), is(expected.toString())); } @@ -760,8 +777,8 @@ public void testMatchFunctionMultipleMatchClauses() { var query = as(field.child(), EsQueryExec.class); assertThat(query.limit().fold(), is(1000)); - var queryStringLeft = QueryBuilders.matchQuery("last_name", "Smith"); - var queryStringRight = QueryBuilders.matchQuery("first_name", "John"); + var queryStringLeft = QueryBuilders.matchQuery("last_name", "Smith").lenient(true); + var queryStringRight = QueryBuilders.matchQuery("first_name", "John").lenient(true); var expected = QueryBuilders.boolQuery().must(queryStringLeft).must(queryStringRight); assertThat(query.query().toString(), is(expected.toString())); } @@ -1306,7 +1323,19 @@ public void testMissingFieldsDoNotGetExtracted() { var projections = project.projections(); assertThat( Expressions.names(projections), - contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary") + contains( + "_meta_field", + "emp_no", + "first_name", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "last_name", + "long_noidx", + "salary" + ) ); // emp_no assertThat(projections.get(1), instanceOf(ReferenceAttribute.class)); @@ -1314,15 +1343,90 @@ public void testMissingFieldsDoNotGetExtracted() { assertThat(projections.get(2), instanceOf(ReferenceAttribute.class)); // last_name --> first_name - var nullAlias = Alias.unwrap(projections.get(7)); + var nullAlias = Alias.unwrap(projections.get(8)); assertThat(Expressions.name(nullAlias), is("first_name")); // salary --> emp_no - nullAlias = Alias.unwrap(projections.get(9)); + nullAlias = Alias.unwrap(projections.get(10)); assertThat(Expressions.name(nullAlias), is("emp_no")); // check field extraction is skipped and that evaled fields are not extracted anymore var field = as(project.child(), FieldExtractExec.class); var fields = field.attributesToExtract(); - assertThat(Expressions.names(fields), contains("_meta_field", "gender", "job", "job.raw", "languages", "long_noidx")); + assertThat(Expressions.names(fields), contains("_meta_field", "gender", "hire_date", "job", "job.raw", "languages", "long_noidx")); + } + + /* + Checks that match filters are pushed down to Lucene when using no casting, for example: + WHERE first_name:"Anna") + WHERE age:17 + WHERE salary:24.5 + */ + public void testSingleMatchOperatorFilterPushdownWithoutCasting() { + checkMatchFunctionPushDown( + (value, dataType) -> DataType.isString(dataType) ? "\"" + value + "\"" : value.toString(), + value -> value, + UNNECESSARY_CASTING_DATA_TYPES, + MATCH_OPERATOR_QUERY + ); + } + + /* + Checks that match filters are pushed down to Lucene when using strings, for example: + WHERE ip:"127.0.0.1" + WHERE date:"2024-07-01" + WHERE date:"8.17.1" + */ + public void testSingleMatchOperatorFilterPushdownWithStringValues() { + checkMatchFunctionPushDown( + (value, dataType) -> "\"" + value + "\"", + Object::toString, + Match.FIELD_DATA_TYPES, + MATCH_OPERATOR_QUERY + ); + } + + /* + Checks that match filters are pushed down to Lucene when using no casting, for example: + WHERE match(first_name, "Anna") + WHERE match(age, 17) + WHERE match(salary, 24.5) + */ + public void testSingleMatchFunctionFilterPushdownWithoutCasting() { + checkMatchFunctionPushDown( + (value, dataType) -> DataType.isString(dataType) ? "\"" + value + "\"" : value.toString(), + value -> value, + UNNECESSARY_CASTING_DATA_TYPES, + MATCH_FUNCTION_QUERY + ); + } + + /* + Checks that match filters are pushed down to Lucene when using casting, for example: + WHERE match(ip, "127.0.0.1"::IP) + WHERE match(date, "2024-07-01"::DATETIME) + WHERE match(date, "8.17.1"::VERSION) + */ + public void testSingleMatchFunctionPushdownWithCasting() { + checkMatchFunctionPushDown( + LocalPhysicalPlanOptimizerTests::queryValueAsCasting, + value -> value, + Match.FIELD_DATA_TYPES, + MATCH_FUNCTION_QUERY + ); + } + + /* + Checks that match filters are pushed down to Lucene when using strings, for example: + WHERE match(ip, "127.0.0.1") + WHERE match(date, "2024-07-01") + WHERE match(date, "8.17.1") + */ + public void testSingleMatchFunctionFilterPushdownWithStringValues() { + checkMatchFunctionPushDown( + (value, dataType) -> "\"" + value + "\"", + Object::toString, + Match.FIELD_DATA_TYPES, + MATCH_FUNCTION_QUERY + ); } /** @@ -1335,20 +1439,68 @@ public void testMissingFieldsDoNotGetExtracted() { * \_EsQueryExec[test], indexMode[standard], query[{"match":{"first_name":{"query":"Anna"}}}][_doc{f}#13], limit[1000], sort[] * estimatedRowSize[324] */ - public void testSingleMatchFilterPushdown() { - var plan = plannerOptimizer.plan(""" - from test - | where first_name:"Anna" - """); + private void checkMatchFunctionPushDown( + BiFunction queryValueProvider, + Function expectedValueProvider, + Collection fieldDataTypes, + String queryFormat + ) { + var analyzer = makeAnalyzer("mapping-all-types.json"); + // Check for every possible query data type + for (DataType fieldDataType : fieldDataTypes) { + var queryValue = randomQueryValue(fieldDataType); + + String fieldName = fieldDataType == DataType.DATETIME ? "date" : fieldDataType.name().toLowerCase(Locale.ROOT); + var esqlQuery = String.format(Locale.ROOT, queryFormat, fieldName, queryValueProvider.apply(queryValue, fieldDataType)); + + try { + var plan = plannerOptimizer.plan(esqlQuery, IS_SV_STATS, analyzer); + var limit = as(plan, LimitExec.class); + var exchange = as(limit.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var fieldExtract = as(project.child(), FieldExtractExec.class); + var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); + + var expectedLuceneQuery = new MatchQueryBuilder(fieldName, expectedValueProvider.apply(queryValue)).lenient(true); + assertThat("Unexpected match query for data type " + fieldDataType, actualLuceneQuery, equalTo(expectedLuceneQuery)); + } catch (ParsingException e) { + fail("Error parsing ESQL query: " + esqlQuery + "\n" + e.getMessage()); + } + } + } - var limit = as(plan, LimitExec.class); - var exchange = as(limit.child(), ExchangeExec.class); - var project = as(exchange.child(), ProjectExec.class); - var fieldExtract = as(project.child(), FieldExtractExec.class); - var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); + private static Object randomQueryValue(DataType dataType) { + return switch (dataType) { + case BOOLEAN -> randomBoolean(); + case INTEGER -> randomInt(); + case LONG -> randomLong(); + case UNSIGNED_LONG -> randomBigInteger(); + case DATE_NANOS -> EsqlDataTypeConverter.nanoTimeToString(randomMillisUpToYear9999()); + case DATETIME -> EsqlDataTypeConverter.dateTimeToString(randomMillisUpToYear9999()); + case DOUBLE -> randomDouble(); + case KEYWORD -> randomAlphaOfLength(5); + case IP -> NetworkAddress.format(randomIp(randomBoolean())); + case TEXT -> randomAlphaOfLength(50); + case VERSION -> VersionUtils.randomVersion(random()).toString(); + default -> throw new IllegalArgumentException("Unexpected type: " + dataType); + }; + } - var expectedLuceneQuery = new MatchQueryBuilder("first_name", "Anna"); - assertThat(actualLuceneQuery, equalTo(expectedLuceneQuery)); + private static String queryValueAsCasting(Object value, DataType dataType) { + if (value instanceof String) { + value = "\"" + value + "\""; + } + return switch (dataType) { + case VERSION -> value + "::VERSION"; + case IP -> value + "::IP"; + case DATETIME -> value + "::DATETIME"; + case DATE_NANOS -> value + "::DATE_NANOS"; + case INTEGER -> value + "::INTEGER"; + case LONG -> value + "::LONG"; + case BOOLEAN -> String.valueOf(value).toLowerCase(Locale.ROOT); + case UNSIGNED_LONG -> "\"" + value + "\"::UNSIGNED_LONG"; + default -> value.toString(); + }; } /** @@ -1384,10 +1536,10 @@ public void testMultipleMatchFilterPushdown() { var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); Source filterSource = new Source(4, 8, "emp_no > 10000"); - var expectedLuceneQuery = new BoolQueryBuilder().must(new MatchQueryBuilder("first_name", "Anna")) - .must(new MatchQueryBuilder("first_name", "Anneke")) + var expectedLuceneQuery = new BoolQueryBuilder().must(new MatchQueryBuilder("first_name", "Anna").lenient(true)) + .must(new MatchQueryBuilder("first_name", "Anneke").lenient(true)) .must(wrapWithSingleQuery(query, QueryBuilders.rangeQuery("emp_no").gt(10000), "emp_no", filterSource)) - .must(new MatchQueryBuilder("last_name", "Xinglin")); + .must(new MatchQueryBuilder("last_name", "Xinglin").lenient(true)); assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString())); } @@ -1420,6 +1572,32 @@ public void testTermFunction() { assertThat(query.query().toString(), is(expected.toString())); } + /** + * Expects + * LimitExec[1000[INTEGER]] + * \_ExchangeExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, hire_date{f}#10, job{f}#11, job.raw{f}#12 + * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, hire_date{f}#10, job{f}#11, job.raw{f}#12 + * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen] + * \_EsQueryExec[test], indexMode[standard], query[{"match":{"emp_no":{"query":123456}}}][_doc{f}#14], + * limit[1000], sort[] estimatedRowSize[332] + */ + public void testMatchWithFieldCasting() { + String query = """ + from test + | where emp_no::long : 123456 + """; + var plan = plannerOptimizer.plan(query); + + var limit = as(plan, LimitExec.class); + var exchange = as(limit.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var fieldExtract = as(project.child(), FieldExtractExec.class); + var queryExec = as(fieldExtract.child(), EsQueryExec.class); + var queryBuilder = as(queryExec.query(), MatchQueryBuilder.class); + assertThat(queryBuilder.fieldName(), is("emp_no")); + assertThat(queryBuilder.value(), is(123456)); + } + private QueryBuilder wrapWithSingleQuery(String query, QueryBuilder inner, String fieldName, Source source) { return FilterTests.singleValueQuery(query, inner, fieldName, source); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index 1f131f79c3d0..c35f01e9fe77 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -611,6 +611,7 @@ public void testExtractorMultiEvalWithDifferentNames() { "emp_no", "first_name", "gender", + "hire_date", "job", "job.raw", "languages", @@ -652,6 +653,7 @@ public void testExtractorMultiEvalWithSameName() { "emp_no", "first_name", "gender", + "hire_date", "job", "job.raw", "languages", @@ -1172,7 +1174,19 @@ public void testPushLimitAndFilterToSource() { assertThat( names(extract.attributesToExtract()), - contains("_meta_field", "emp_no", "first_name", "gender", "job", "job.raw", "languages", "last_name", "long_noidx", "salary") + contains( + "_meta_field", + "emp_no", + "first_name", + "gender", + "hire_date", + "job", + "job.raw", + "languages", + "last_name", + "long_noidx", + "salary" + ) ); var source = source(extract.child()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java index 69c00eb395fd..b83892ea4704 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.FilteredExpression; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; +import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToInteger; import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike; import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLike; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add; @@ -2314,7 +2315,6 @@ public void testInvalidMatchOperator() { "from test | WHERE field:CONCAT(\"hello\", \"world\")", "line 1:25: mismatched input 'CONCAT' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, " ); - expectError("from test | WHERE field:123::STRING", "line 1:28: mismatched input '::' expecting {, '|', 'and', 'or'}"); expectError( "from test | WHERE field:(true OR false)", "line 1:25: extraneous input '(' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, " @@ -2323,7 +2323,7 @@ public void testInvalidMatchOperator() { "from test | WHERE field:another_field_or_value", "line 1:25: mismatched input 'another_field_or_value' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, " ); - expectError("from test | WHERE field:2+3", "line 1:26: mismatched input '+' expecting {, '|', 'and', 'or'}"); + expectError("from test | WHERE field:2+3", "line 1:26: mismatched input '+'"); expectError( "from test | WHERE \"field\":\"value\"", "line 1:26: mismatched input ':' expecting {, '|', 'and', '::', 'or', '+', '-', '*', '/', '%'}" @@ -2333,4 +2333,24 @@ public void testInvalidMatchOperator() { "line 1:37: mismatched input ':' expecting {, '|', 'and', '::', 'or', '+', '-', '*', '/', '%'}" ); } + + public void testMatchFunctionFieldCasting() { + var plan = statement("FROM test | WHERE match(field::int, \"value\")"); + var filter = as(plan, Filter.class); + var function = (UnresolvedFunction) filter.condition(); + var toInteger = (ToInteger) function.children().get(0); + var matchField = (UnresolvedAttribute) toInteger.field(); + assertThat(matchField.name(), equalTo("field")); + assertThat(function.children().get(1).fold(), equalTo("value")); + } + + public void testMatchOperatorFieldCasting() { + var plan = statement("FROM test | WHERE field::int : \"value\""); + var filter = as(plan, Filter.class); + var match = (Match) filter.condition(); + var toInteger = (ToInteger) match.field(); + var matchField = (UnresolvedAttribute) toInteger.field(); + assertThat(matchField.name(), equalTo("field")); + assertThat(match.query().fold(), equalTo("value")); + } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml index 2cd1595d2d5b..663c0dc78acb 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml @@ -6,7 +6,6 @@ setup: path: /_query parameters: [ method, path, parameters, capabilities ] capabilities: [ match_operator_colon ] - cluster_features: [ "gte_v8.16.0" ] reason: "Match operator added in 8.16.0" test_runner_features: [capabilities, allowed_warnings_regex] - do: @@ -95,6 +94,25 @@ setup: - length: { values: 1 } - match: { values.0.0: 5 } +--- +"match with integer field": + - requires: + capabilities: + - method: POST + path: /_query + parameters: [ method, path, parameters, capabilities ] + capabilities: [ match_additional_types ] + reason: "Additional types support for match" + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'FROM test | WHERE id:3 | KEEP id' + + - length: { values: 1 } + - match: { values.0.0: 3 } + --- "match on non existing column": - do: @@ -178,17 +196,3 @@ setup: - match: { status: 400 } - match: { error.type: verification_exception } - match: { error.reason: "Found 1 problem\nline 1:34: [:] operator is only supported in WHERE commands" } - ---- -"match with non text field": - - do: - catch: bad_request - allowed_warnings_regex: - - "No limit defined, adding default limit of \\[.*\\]" - esql.query: - body: - query: 'FROM test | WHERE id:"fox"' - - - match: { status: 400 } - - match: { error.type: verification_exception } - - match: { error.reason: "Found 1 problem\nline 1:19: first argument of [id:\"fox\"] must be [string], found value [id] type [integer]" } From d614804731fc97c468d911ad6e462206103755f0 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 9 Dec 2024 15:30:14 -0500 Subject: [PATCH 29/39] Remove analyzer version deprecation check (#118167) Version has been deprecated since v7: https://github.com/elastic/elasticsearch/pull/74073 Removing checking for the version setting. It has been ignored and does nothing for the entirety of 8 and for the last minors of v7. --- .../common/ASCIIFoldingTokenFilterFactory.java | 2 +- .../AbstractCompoundWordTokenFilterFactory.java | 2 +- .../common/ApostropheFilterFactory.java | 2 +- .../analysis/common/ArabicAnalyzerProvider.java | 2 +- .../ArabicNormalizationFilterFactory.java | 2 +- .../common/ArabicStemTokenFilterFactory.java | 2 +- .../common/ArmenianAnalyzerProvider.java | 2 +- .../analysis/common/BasqueAnalyzerProvider.java | 2 +- .../common/BengaliAnalyzerProvider.java | 2 +- .../BengaliNormalizationFilterFactory.java | 2 +- .../common/BrazilianAnalyzerProvider.java | 2 +- .../common/BrazilianStemTokenFilterFactory.java | 2 +- .../common/BulgarianAnalyzerProvider.java | 2 +- .../analysis/common/CJKBigramFilterFactory.java | 2 +- .../analysis/common/CJKWidthFilterFactory.java | 2 +- .../common/CatalanAnalyzerProvider.java | 2 +- .../common/CharGroupTokenizerFactory.java | 2 +- .../common/ChineseAnalyzerProvider.java | 2 +- .../analysis/common/CjkAnalyzerProvider.java | 2 +- .../analysis/common/ClassicFilterFactory.java | 2 +- .../common/ClassicTokenizerFactory.java | 2 +- .../common/CommonGramsTokenFilterFactory.java | 2 +- .../analysis/common/CzechAnalyzerProvider.java | 2 +- .../common/CzechStemTokenFilterFactory.java | 2 +- .../analysis/common/DanishAnalyzerProvider.java | 2 +- .../common/DecimalDigitFilterFactory.java | 2 +- .../DelimitedPayloadTokenFilterFactory.java | 2 +- .../analysis/common/DutchAnalyzerProvider.java | 2 +- .../common/DutchStemTokenFilterFactory.java | 2 +- .../common/EdgeNGramTokenFilterFactory.java | 2 +- .../common/EdgeNGramTokenizerFactory.java | 2 +- .../common/ElisionTokenFilterFactory.java | 2 +- .../common/EnglishAnalyzerProvider.java | 2 +- .../common/EstonianAnalyzerProvider.java | 2 +- .../common/FingerprintAnalyzerProvider.java | 2 +- .../common/FingerprintTokenFilterFactory.java | 2 +- .../common/FinnishAnalyzerProvider.java | 2 +- .../common/FlattenGraphTokenFilterFactory.java | 2 +- .../analysis/common/FrenchAnalyzerProvider.java | 2 +- .../common/FrenchStemTokenFilterFactory.java | 2 +- .../common/GalicianAnalyzerProvider.java | 2 +- .../analysis/common/GermanAnalyzerProvider.java | 2 +- .../GermanNormalizationFilterFactory.java | 2 +- .../common/GermanStemTokenFilterFactory.java | 2 +- .../analysis/common/GreekAnalyzerProvider.java | 2 +- .../analysis/common/HindiAnalyzerProvider.java | 2 +- .../common/HindiNormalizationFilterFactory.java | 2 +- .../common/HungarianAnalyzerProvider.java | 2 +- .../common/IndicNormalizationFilterFactory.java | 2 +- .../common/IndonesianAnalyzerProvider.java | 2 +- .../analysis/common/IrishAnalyzerProvider.java | 2 +- .../common/ItalianAnalyzerProvider.java | 2 +- .../common/KStemTokenFilterFactory.java | 2 +- .../analysis/common/KeepTypesFilterFactory.java | 2 +- .../analysis/common/KeepWordFilterFactory.java | 2 +- .../common/KeywordAnalyzerProvider.java | 2 +- .../common/KeywordMarkerTokenFilterFactory.java | 2 +- .../common/KeywordTokenizerFactory.java | 2 +- .../common/LatvianAnalyzerProvider.java | 2 +- .../common/LengthTokenFilterFactory.java | 2 +- .../analysis/common/LetterTokenizerFactory.java | 2 +- .../common/LimitTokenCountFilterFactory.java | 2 +- .../common/LithuanianAnalyzerProvider.java | 2 +- .../common/LowerCaseTokenFilterFactory.java | 2 +- .../common/MinHashTokenFilterFactory.java | 2 +- .../common/MultiplexerTokenFilterFactory.java | 2 +- .../common/NGramTokenFilterFactory.java | 2 +- .../analysis/common/NGramTokenizerFactory.java | 2 +- .../common/NorwegianAnalyzerProvider.java | 2 +- .../common/PathHierarchyTokenizerFactory.java | 2 +- .../common/PatternAnalyzerProvider.java | 2 +- .../PatternCaptureGroupTokenFilterFactory.java | 2 +- .../PatternReplaceTokenFilterFactory.java | 2 +- .../common/PatternTokenizerFactory.java | 2 +- .../common/PersianAnalyzerProvider.java | 2 +- .../PersianNormalizationFilterFactory.java | 2 +- .../common/PersianStemTokenFilterFactory.java | 2 +- .../common/PorterStemTokenFilterFactory.java | 2 +- .../common/PortugueseAnalyzerProvider.java | 2 +- .../PredicateTokenFilterScriptFactory.java | 2 +- .../RemoveDuplicatesTokenFilterFactory.java | 2 +- .../common/ReverseTokenFilterFactory.java | 2 +- .../common/RomanianAnalyzerProvider.java | 2 +- .../common/RussianAnalyzerProvider.java | 2 +- .../common/RussianStemTokenFilterFactory.java | 2 +- .../ScandinavianFoldingFilterFactory.java | 2 +- .../ScandinavianNormalizationFilterFactory.java | 2 +- .../ScriptedConditionTokenFilterFactory.java | 2 +- .../common/SerbianAnalyzerProvider.java | 2 +- .../SerbianNormalizationFilterFactory.java | 2 +- .../analysis/common/SimpleAnalyzerProvider.java | 2 +- .../SimplePatternSplitTokenizerFactory.java | 2 +- .../common/SimplePatternTokenizerFactory.java | 2 +- .../common/SnowballAnalyzerProvider.java | 2 +- .../common/SnowballTokenFilterFactory.java | 2 +- .../analysis/common/SoraniAnalyzerProvider.java | 2 +- .../SoraniNormalizationFilterFactory.java | 2 +- .../common/SpanishAnalyzerProvider.java | 2 +- .../StemmerOverrideTokenFilterFactory.java | 2 +- .../common/StemmerTokenFilterFactory.java | 2 +- .../analysis/common/StopAnalyzerProvider.java | 2 +- .../common/SwedishAnalyzerProvider.java | 2 +- .../common/SynonymTokenFilterFactory.java | 2 +- .../analysis/common/ThaiAnalyzerProvider.java | 2 +- .../analysis/common/ThaiTokenizerFactory.java | 2 +- .../analysis/common/TrimTokenFilterFactory.java | 2 +- .../common/TruncateTokenFilterFactory.java | 2 +- .../common/TurkishAnalyzerProvider.java | 2 +- .../common/UAX29URLEmailTokenizerFactory.java | 2 +- .../common/UniqueTokenFilterFactory.java | 2 +- .../common/UpperCaseTokenFilterFactory.java | 2 +- .../common/WhitespaceAnalyzerProvider.java | 2 +- .../common/WhitespaceTokenizerFactory.java | 2 +- .../WordDelimiterGraphTokenFilterFactory.java | 2 +- .../common/WordDelimiterTokenFilterFactory.java | 2 +- .../common/XLowerCaseTokenizerFactory.java | 2 +- .../analysis/common/CompoundAnalysisTests.java | 4 ---- .../analysis/icu/IcuAnalyzerProvider.java | 2 +- .../icu/IcuCollationTokenFilterFactory.java | 2 +- .../icu/IcuFoldingTokenFilterFactory.java | 2 +- .../icu/IcuNormalizerTokenFilterFactory.java | 2 +- .../analysis/icu/IcuTokenizerFactory.java | 2 +- .../icu/IcuTransformTokenFilterFactory.java | 2 +- .../HiraganaUppercaseFilterFactory.java | 2 +- .../JapaneseStopTokenFilterFactory.java | 2 +- .../KatakanaUppercaseFilterFactory.java | 2 +- .../kuromoji/KuromojiAnalyzerProvider.java | 2 +- .../kuromoji/KuromojiBaseFormFilterFactory.java | 2 +- .../KuromojiCompletionAnalyzerProvider.java | 2 +- .../KuromojiCompletionFilterFactory.java | 2 +- .../KuromojiKatakanaStemmerFactory.java | 2 +- .../kuromoji/KuromojiNumberFilterFactory.java | 2 +- .../KuromojiPartOfSpeechFilterFactory.java | 2 +- .../KuromojiReadingFormFilterFactory.java | 2 +- .../kuromoji/KuromojiTokenizerFactory.java | 2 +- .../analysis/nori/NoriAnalyzerProvider.java | 2 +- .../analysis/nori/NoriNumberFilterFactory.java | 2 +- .../nori/NoriPartOfSpeechStopFilterFactory.java | 2 +- .../nori/NoriReadingFormFilterFactory.java | 2 +- .../analysis/nori/NoriTokenizerFactory.java | 2 +- .../phonetic/PhoneticTokenFilterFactory.java | 2 +- .../smartcn/SmartChineseAnalyzerProvider.java | 2 +- .../SmartChineseNoOpTokenFilterFactory.java | 2 +- .../SmartChineseStopTokenFilterFactory.java | 2 +- .../SmartChineseTokenizerTokenizerFactory.java | 2 +- .../analysis/pl/PolishAnalyzerProvider.java | 2 +- .../pl/PolishStemTokenFilterFactory.java | 2 +- .../pl/PolishStopTokenFilterFactory.java | 2 +- .../ukrainian/UkrainianAnalyzerProvider.java | 2 +- .../indices/recovery/IndexRecoveryIT.java | 17 +++++++---------- .../subphase/highlight/HighlighterSearchIT.java | 9 +++++---- .../analysis/AbstractIndexAnalyzerProvider.java | 4 +--- .../analysis/AbstractTokenFilterFactory.java | 5 +---- .../analysis/AbstractTokenizerFactory.java | 6 +----- .../elasticsearch/index/analysis/Analysis.java | 14 -------------- .../index/analysis/CustomAnalyzerProvider.java | 2 +- .../analysis/CustomNormalizerProvider.java | 2 +- .../analysis/HunspellTokenFilterFactory.java | 2 +- .../analysis/LowercaseNormalizerProvider.java | 2 +- .../analysis/ShingleTokenFilterFactory.java | 2 +- .../analysis/StandardAnalyzerProvider.java | 2 +- .../analysis/StandardTokenizerFactory.java | 2 +- .../index/analysis/StopTokenFilterFactory.java | 2 +- .../indices/analysis/AnalysisModule.java | 2 +- .../indices/TransportAnalyzeActionTests.java | 2 +- .../index/analysis/AnalysisRegistryTests.java | 6 +++--- .../analysis/ReloadableCustomAnalyzerTests.java | 4 ++-- .../index/analysis/StopTokenFilterTests.java | 17 ----------------- .../mapper/TextFieldAnalyzerModeTests.java | 2 +- .../indices/analysis/AnalysisModuleTests.java | 1 - .../analysis/MyFilterTokenFilterFactory.java | 2 +- .../org/elasticsearch/analysis/common/test1.yml | 1 - .../MlClassicTokenizerFactory.java | 2 +- .../MlStandardTokenizerFactory.java | 2 +- 174 files changed, 182 insertions(+), 230 deletions(-) diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java index ba4333bf1be0..bf59fe49d075 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java @@ -30,7 +30,7 @@ public class ASCIIFoldingTokenFilterFactory extends AbstractTokenFilterFactory i private final boolean preserveOriginal; public ASCIIFoldingTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); preserveOriginal = settings.getAsBoolean(PRESERVE_ORIGINAL.getPreferredName(), DEFAULT_PRESERVE_ORIGINAL); } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java index 1b68b2a6ebaf..c0a8c1374c14 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java @@ -30,7 +30,7 @@ public abstract class AbstractCompoundWordTokenFilterFactory extends AbstractTok protected final CharArraySet wordList; protected AbstractCompoundWordTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); minWordSize = settings.getAsInt("min_word_size", CompoundWordTokenFilterBase.DEFAULT_MIN_WORD_SIZE); minSubwordSize = settings.getAsInt("min_subword_size", CompoundWordTokenFilterBase.DEFAULT_MIN_SUBWORD_SIZE); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ApostropheFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ApostropheFilterFactory.java index 97a5de3a1408..28404ac45343 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ApostropheFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ApostropheFilterFactory.java @@ -21,7 +21,7 @@ public class ApostropheFilterFactory extends AbstractTokenFilterFactory { ApostropheFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ArabicAnalyzerProvider.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ArabicAnalyzerProvider.java index 4e021f8138e7..34869c709a42 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ArabicAnalyzerProvider.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ArabicAnalyzerProvider.java @@ -22,7 +22,7 @@ public class ArabicAnalyzerProvider extends AbstractIndexAnalyzerProvider asArray = settings.getAsList("ignored_scripts"); Set scripts = new HashSet<>(Arrays.asList("han", "hiragana", "katakana", "hangul")); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKWidthFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKWidthFilterFactory.java index 07e28c5a4924..717bfc1191ab 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKWidthFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKWidthFilterFactory.java @@ -20,7 +20,7 @@ public final class CJKWidthFilterFactory extends AbstractTokenFilterFactory implements NormalizingTokenFilterFactory { CJKWidthFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CatalanAnalyzerProvider.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CatalanAnalyzerProvider.java index 2be6f220e444..b134a1d2ab0f 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CatalanAnalyzerProvider.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CatalanAnalyzerProvider.java @@ -22,7 +22,7 @@ public class CatalanAnalyzerProvider extends AbstractIndexAnalyzerProvider arrayKeepTypes = settings.getAsList(KEEP_TYPES_KEY, null); if ((arrayKeepTypes == null)) { diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeepWordFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeepWordFilterFactory.java index 0fa763d627a7..e2be82df1060 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeepWordFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeepWordFilterFactory.java @@ -51,7 +51,7 @@ public class KeepWordFilterFactory extends AbstractTokenFilterFactory { private static final String ENABLE_POS_INC_KEY = "enable_position_increments"; KeepWordFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); final List arrayKeepWords = settings.getAsList(KEEP_WORDS_KEY, null); final String keepWordsPath = settings.get(KEEP_WORDS_PATH_KEY, null); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeywordAnalyzerProvider.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeywordAnalyzerProvider.java index c43327cf508c..c4cd39c4f441 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeywordAnalyzerProvider.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/KeywordAnalyzerProvider.java @@ -20,7 +20,7 @@ public class KeywordAnalyzerProvider extends AbstractIndexAnalyzerProvider regexes = settings.getAsList(PATTERNS_KEY, null, false); if (regexes == null) { throw new IllegalArgumentException("required setting '" + PATTERNS_KEY + "' is missing for token filter [" + name + "]"); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternReplaceTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternReplaceTokenFilterFactory.java index cd15b05aabfb..d90b7a182cab 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternReplaceTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternReplaceTokenFilterFactory.java @@ -27,7 +27,7 @@ public class PatternReplaceTokenFilterFactory extends AbstractTokenFilterFactory private final boolean all; public PatternReplaceTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); String sPattern = settings.get("pattern", null); if (sPattern == null) { diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternTokenizerFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternTokenizerFactory.java index b8f2e194c2ca..311d63f4d011 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternTokenizerFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PatternTokenizerFactory.java @@ -25,7 +25,7 @@ public class PatternTokenizerFactory extends AbstractTokenizerFactory { private final int group; PatternTokenizerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); String sPattern = settings.get("pattern", "\\W+" /*PatternAnalyzer.NON_WORD_PATTERN*/); if (sPattern == null) { diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PersianAnalyzerProvider.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PersianAnalyzerProvider.java index 917a45188123..b4cb8b800309 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PersianAnalyzerProvider.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PersianAnalyzerProvider.java @@ -35,7 +35,7 @@ public class PersianAnalyzerProvider extends AbstractIndexAnalyzerProvider filterNames; ScriptedConditionTokenFilterFactory(IndexSettings indexSettings, String name, Settings settings, ScriptService scriptService) { - super(name, settings); + super(name); Settings scriptSettings = settings.getAsSettings("script"); Script script = Script.parse(scriptSettings); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/SerbianAnalyzerProvider.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/SerbianAnalyzerProvider.java index 6dc899be9587..e36aaa9756e7 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/SerbianAnalyzerProvider.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/SerbianAnalyzerProvider.java @@ -22,7 +22,7 @@ public class SerbianAnalyzerProvider extends AbstractIndexAnalyzerProvider rules = Analysis.getWordList(env, settings, "rules"); if (rules == null) { diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactory.java index 7548c8ad2b88..a6e9fccd9d09 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StemmerTokenFilterFactory.java @@ -92,7 +92,7 @@ public class StemmerTokenFilterFactory extends AbstractTokenFilterFactory { private static final DeprecationLogger DEPRECATION_LOGGER = DeprecationLogger.getLogger(StemmerTokenFilterFactory.class); StemmerTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) throws IOException { - super(name, settings); + super(name); this.language = Strings.capitalize(settings.get("language", settings.get("name", "porter"))); // check that we have a valid language by trying to create a TokenStream create(EMPTY_TOKEN_STREAM).close(); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StopAnalyzerProvider.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StopAnalyzerProvider.java index 0977d08d0fd4..983a9410fba2 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StopAnalyzerProvider.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/StopAnalyzerProvider.java @@ -23,7 +23,7 @@ public class StopAnalyzerProvider extends AbstractIndexAnalyzerProvider DIGIT diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java index 083594f6ab02..d20a837e92bf 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java @@ -47,7 +47,7 @@ public class WordDelimiterTokenFilterFactory extends AbstractTokenFilterFactory @SuppressWarnings("HiddenField") public WordDelimiterTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); // Sample Format for the type table: // $ => DIGIT diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/XLowerCaseTokenizerFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/XLowerCaseTokenizerFactory.java index 7fc1bf688235..211278d0ece4 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/XLowerCaseTokenizerFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/XLowerCaseTokenizerFactory.java @@ -20,7 +20,7 @@ public class XLowerCaseTokenizerFactory extends AbstractTokenizerFactory { public XLowerCaseTokenizerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); } @Override diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CompoundAnalysisTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CompoundAnalysisTests.java index 69dd8e91b52b..92c134b5c0f5 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CompoundAnalysisTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CompoundAnalysisTests.java @@ -64,7 +64,6 @@ public void testDictionaryDecompounder() throws Exception { hasItems("donau", "dampf", "schiff", "donaudampfschiff", "spargel", "creme", "suppe", "spargelcremesuppe") ); } - assertWarnings("Setting [version] on analysis component [custom7] has no effect and is deprecated"); } public void testHyphenationDecompoundingAnalyzerOnlyLongestMatch() throws Exception { @@ -76,7 +75,6 @@ public void testHyphenationDecompoundingAnalyzerOnlyLongestMatch() throws Except hasItems("kaffeemaschine", "kaffee", "fee", "maschine", "fussballpumpe", "fussball", "ballpumpe", "pumpe") ); } - assertWarnings("Setting [version] on analysis component [custom7] has no effect and is deprecated"); } /** @@ -89,7 +87,6 @@ public void testHyphenationDecompoundingAnalyzerNoSubMatches() throws Exception List terms = analyze(settings, "hyphenationDecompoundingAnalyzerNoSubMatches", "kaffeemaschine fussballpumpe"); MatcherAssert.assertThat(terms, hasItems("kaffeemaschine", "kaffee", "maschine", "fussballpumpe", "fussball", "ballpumpe")); } - assertWarnings("Setting [version] on analysis component [custom7] has no effect and is deprecated"); } /** @@ -102,7 +99,6 @@ public void testHyphenationDecompoundingAnalyzerNoOverlappingMatches() throws Ex List terms = analyze(settings, "hyphenationDecompoundingAnalyzerNoOverlappingMatches", "kaffeemaschine fussballpumpe"); MatcherAssert.assertThat(terms, hasItems("kaffeemaschine", "kaffee", "maschine", "fussballpumpe", "fussball", "pumpe")); } - assertWarnings("Setting [version] on analysis component [custom7] has no effect and is deprecated"); } private List analyze(Settings settings, String analyzerName, String text) throws IOException { diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuAnalyzerProvider.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuAnalyzerProvider.java index 3fea1918252e..9fb611345dbe 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuAnalyzerProvider.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuAnalyzerProvider.java @@ -28,7 +28,7 @@ public class IcuAnalyzerProvider extends AbstractIndexAnalyzerProvider private final Normalizer2 normalizer; public IcuAnalyzerProvider(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); String method = settings.get("method", "nfkc_cf"); String mode = settings.get("mode", "compose"); if ("compose".equals(mode) == false && "decompose".equals(mode) == false) { diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuCollationTokenFilterFactory.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuCollationTokenFilterFactory.java index ae8ead523b7e..fe0b3a00b2bb 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuCollationTokenFilterFactory.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuCollationTokenFilterFactory.java @@ -44,7 +44,7 @@ public class IcuCollationTokenFilterFactory extends AbstractTokenFilterFactory { @SuppressWarnings("HiddenField") public IcuCollationTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); Collator collator; String rules = settings.get("rules"); diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuFoldingTokenFilterFactory.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuFoldingTokenFilterFactory.java index 6cffbb7e0a17..8932518dc543 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuFoldingTokenFilterFactory.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuFoldingTokenFilterFactory.java @@ -39,7 +39,7 @@ public class IcuFoldingTokenFilterFactory extends AbstractTokenFilterFactory imp private final Normalizer2 normalizer; public IcuFoldingTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); this.normalizer = IcuNormalizerTokenFilterFactory.wrapWithUnicodeSetFilter(ICU_FOLDING_NORMALIZER, settings); } diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuNormalizerTokenFilterFactory.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuNormalizerTokenFilterFactory.java index 23b2c355b7a6..c9eceef30f62 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuNormalizerTokenFilterFactory.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuNormalizerTokenFilterFactory.java @@ -30,7 +30,7 @@ public class IcuNormalizerTokenFilterFactory extends AbstractTokenFilterFactory private final Normalizer2 normalizer; public IcuNormalizerTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); String method = settings.get("name", "nfkc_cf"); Normalizer2 normalizerInstance = Normalizer2.getInstance(null, method, Normalizer2.Mode.COMPOSE); this.normalizer = wrapWithUnicodeSetFilter(normalizerInstance, settings); diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTokenizerFactory.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTokenizerFactory.java index 62ab6d879290..c66d25ffa2f3 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTokenizerFactory.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTokenizerFactory.java @@ -39,7 +39,7 @@ public class IcuTokenizerFactory extends AbstractTokenizerFactory { private static final String RULE_FILES = "rule_files"; public IcuTokenizerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); config = getIcuConfig(environment, settings); } diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTransformTokenFilterFactory.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTransformTokenFilterFactory.java index 785b083c4c31..5a0a0b3897a4 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTransformTokenFilterFactory.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/IcuTransformTokenFilterFactory.java @@ -26,7 +26,7 @@ public class IcuTransformTokenFilterFactory extends AbstractTokenFilterFactory i private final Transliterator transliterator; public IcuTransformTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); this.id = settings.get("id", "Null"); String s = settings.get("dir", "forward"); this.dir = "forward".equals(s) ? Transliterator.FORWARD : Transliterator.REVERSE; diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/HiraganaUppercaseFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/HiraganaUppercaseFilterFactory.java index b22757af2237..2d761d99e742 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/HiraganaUppercaseFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/HiraganaUppercaseFilterFactory.java @@ -18,7 +18,7 @@ public class HiraganaUppercaseFilterFactory extends AbstractTokenFilterFactory { public HiraganaUppercaseFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/JapaneseStopTokenFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/JapaneseStopTokenFilterFactory.java index e34d5246dd09..3a26e647092f 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/JapaneseStopTokenFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/JapaneseStopTokenFilterFactory.java @@ -35,7 +35,7 @@ public class JapaneseStopTokenFilterFactory extends AbstractTokenFilterFactory { private final boolean removeTrailing; public JapaneseStopTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); this.ignoreCase = settings.getAsBoolean("ignore_case", false); this.removeTrailing = settings.getAsBoolean("remove_trailing", true); this.stopWords = Analysis.parseWords( diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KatakanaUppercaseFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KatakanaUppercaseFilterFactory.java index 1f72f1d57d2c..0776c4ca970a 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KatakanaUppercaseFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KatakanaUppercaseFilterFactory.java @@ -18,7 +18,7 @@ public class KatakanaUppercaseFilterFactory extends AbstractTokenFilterFactory { public KatakanaUppercaseFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiAnalyzerProvider.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiAnalyzerProvider.java index db336bec997c..f0667da992be 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiAnalyzerProvider.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiAnalyzerProvider.java @@ -26,7 +26,7 @@ public class KuromojiAnalyzerProvider extends AbstractIndexAnalyzerProvider stopWords = Analysis.parseStopWords(env, settings, JapaneseAnalyzer.getDefaultStopSet()); final JapaneseTokenizer.Mode mode = KuromojiTokenizerFactory.getMode(settings); final UserDictionary userDictionary = KuromojiTokenizerFactory.getUserDictionary(env, settings); diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiBaseFormFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiBaseFormFilterFactory.java index 536e6fe993d0..2e8635704c6b 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiBaseFormFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiBaseFormFilterFactory.java @@ -19,7 +19,7 @@ public class KuromojiBaseFormFilterFactory extends AbstractTokenFilterFactory { public KuromojiBaseFormFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionAnalyzerProvider.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionAnalyzerProvider.java index c4970251d2bb..f7121fb211ac 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionAnalyzerProvider.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionAnalyzerProvider.java @@ -22,7 +22,7 @@ public class KuromojiCompletionAnalyzerProvider extends AbstractIndexAnalyzerPro private final JapaneseCompletionAnalyzer analyzer; public KuromojiCompletionAnalyzerProvider(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); final UserDictionary userDictionary = KuromojiTokenizerFactory.getUserDictionary(env, settings); final Mode mode = KuromojiCompletionFilterFactory.getMode(settings); analyzer = new JapaneseCompletionAnalyzer(userDictionary, mode); diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionFilterFactory.java index 3ec6d08145e6..a7a2d984c3bb 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiCompletionFilterFactory.java @@ -22,7 +22,7 @@ public class KuromojiCompletionFilterFactory extends AbstractTokenFilterFactory private final Mode mode; public KuromojiCompletionFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); mode = getMode(settings); } diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiKatakanaStemmerFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiKatakanaStemmerFactory.java index 7c4d3138381f..c06f8b7a4941 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiKatakanaStemmerFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiKatakanaStemmerFactory.java @@ -21,7 +21,7 @@ public class KuromojiKatakanaStemmerFactory extends AbstractTokenFilterFactory { private final int minimumLength; public KuromojiKatakanaStemmerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); minimumLength = settings.getAsInt("minimum_length", JapaneseKatakanaStemFilter.DEFAULT_MINIMUM_LENGTH); } diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiNumberFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiNumberFilterFactory.java index 089b7e10bfae..605d2c66fac1 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiNumberFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiNumberFilterFactory.java @@ -18,7 +18,7 @@ public class KuromojiNumberFilterFactory extends AbstractTokenFilterFactory { public KuromojiNumberFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiPartOfSpeechFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiPartOfSpeechFilterFactory.java index e8efa781726f..5ec3023ca484 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiPartOfSpeechFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiPartOfSpeechFilterFactory.java @@ -27,7 +27,7 @@ public class KuromojiPartOfSpeechFilterFactory extends AbstractTokenFilterFactor private final Set stopTags = new HashSet<>(); public KuromojiPartOfSpeechFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); List wordList = Analysis.getWordList(env, settings, "stoptags"); if (wordList != null) { stopTags.addAll(wordList); diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiReadingFormFilterFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiReadingFormFilterFactory.java index 09ab0bbd4b8d..8e9f03a0c626 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiReadingFormFilterFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiReadingFormFilterFactory.java @@ -21,7 +21,7 @@ public class KuromojiReadingFormFilterFactory extends AbstractTokenFilterFactory private final boolean useRomaji; public KuromojiReadingFormFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); useRomaji = settings.getAsBoolean("use_romaji", false); } diff --git a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiTokenizerFactory.java b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiTokenizerFactory.java index edb29a8f4c98..aa978e3e7387 100644 --- a/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiTokenizerFactory.java +++ b/plugins/analysis-kuromoji/src/main/java/org/elasticsearch/plugin/analysis/kuromoji/KuromojiTokenizerFactory.java @@ -44,7 +44,7 @@ public class KuromojiTokenizerFactory extends AbstractTokenizerFactory { private boolean discardCompoundToken; public KuromojiTokenizerFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); mode = getMode(settings); userDictionary = getUserDictionary(env, settings); discardPunctuation = settings.getAsBoolean("discard_punctuation", true); diff --git a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriAnalyzerProvider.java b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriAnalyzerProvider.java index 180f6aa0a7f9..36094a4f46df 100644 --- a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriAnalyzerProvider.java +++ b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriAnalyzerProvider.java @@ -29,7 +29,7 @@ public class NoriAnalyzerProvider extends AbstractIndexAnalyzerProvider tagList = Analysis.getWordList(env, settings, "stoptags"); diff --git a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriNumberFilterFactory.java b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriNumberFilterFactory.java index cbe92156cd76..ac64c5ff2642 100644 --- a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriNumberFilterFactory.java +++ b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriNumberFilterFactory.java @@ -19,7 +19,7 @@ public class NoriNumberFilterFactory extends AbstractTokenFilterFactory { public NoriNumberFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriPartOfSpeechStopFilterFactory.java b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriPartOfSpeechStopFilterFactory.java index dddb485ab0df..f93a1bd6e909 100644 --- a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriPartOfSpeechStopFilterFactory.java +++ b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriPartOfSpeechStopFilterFactory.java @@ -26,7 +26,7 @@ public class NoriPartOfSpeechStopFilterFactory extends AbstractTokenFilterFactor private final Set stopTags; public NoriPartOfSpeechStopFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); List tagList = Analysis.getWordList(env, settings, "stoptags"); this.stopTags = tagList != null ? resolvePOSList(tagList) : KoreanPartOfSpeechStopFilter.DEFAULT_STOP_TAGS; } diff --git a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriReadingFormFilterFactory.java b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriReadingFormFilterFactory.java index 1e1b211fdea0..ad5a4a6d1b21 100644 --- a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriReadingFormFilterFactory.java +++ b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriReadingFormFilterFactory.java @@ -18,7 +18,7 @@ public class NoriReadingFormFilterFactory extends AbstractTokenFilterFactory { public NoriReadingFormFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriTokenizerFactory.java b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriTokenizerFactory.java index ed8458bc9404..40e159b34385 100644 --- a/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriTokenizerFactory.java +++ b/plugins/analysis-nori/src/main/java/org/elasticsearch/plugin/analysis/nori/NoriTokenizerFactory.java @@ -38,7 +38,7 @@ public class NoriTokenizerFactory extends AbstractTokenizerFactory { private final boolean discardPunctuation; public NoriTokenizerFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); decompoundMode = getMode(settings); userDictionary = getUserDictionary(env, settings, indexSettings); discardPunctuation = settings.getAsBoolean("discard_punctuation", true); diff --git a/plugins/analysis-phonetic/src/main/java/org/elasticsearch/plugin/analysis/phonetic/PhoneticTokenFilterFactory.java b/plugins/analysis-phonetic/src/main/java/org/elasticsearch/plugin/analysis/phonetic/PhoneticTokenFilterFactory.java index 786c6230349a..60986fe58db6 100644 --- a/plugins/analysis-phonetic/src/main/java/org/elasticsearch/plugin/analysis/phonetic/PhoneticTokenFilterFactory.java +++ b/plugins/analysis-phonetic/src/main/java/org/elasticsearch/plugin/analysis/phonetic/PhoneticTokenFilterFactory.java @@ -46,7 +46,7 @@ public class PhoneticTokenFilterFactory extends AbstractTokenFilterFactory { private boolean isDaitchMokotoff; public PhoneticTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); this.languageset = null; this.nametype = null; this.ruletype = null; diff --git a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseAnalyzerProvider.java b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseAnalyzerProvider.java index 668b94d0ca97..9dceac60b3c3 100644 --- a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseAnalyzerProvider.java +++ b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseAnalyzerProvider.java @@ -20,7 +20,7 @@ public class SmartChineseAnalyzerProvider extends AbstractIndexAnalyzerProvider< private final SmartChineseAnalyzer analyzer; public SmartChineseAnalyzerProvider(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); analyzer = new SmartChineseAnalyzer(SmartChineseAnalyzer.getDefaultStopSet()); } diff --git a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseNoOpTokenFilterFactory.java b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseNoOpTokenFilterFactory.java index 55cda6785227..41869b1a7222 100644 --- a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseNoOpTokenFilterFactory.java +++ b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseNoOpTokenFilterFactory.java @@ -18,7 +18,7 @@ public class SmartChineseNoOpTokenFilterFactory extends AbstractTokenFilterFactory { public SmartChineseNoOpTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(name, settings); + super(name); } @Override diff --git a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseStopTokenFilterFactory.java b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseStopTokenFilterFactory.java index 2463ad4a2c18..5688971d0286 100644 --- a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseStopTokenFilterFactory.java +++ b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseStopTokenFilterFactory.java @@ -35,7 +35,7 @@ public class SmartChineseStopTokenFilterFactory extends AbstractTokenFilterFacto private final boolean removeTrailing; public SmartChineseStopTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, settings); + super(name); this.ignoreCase = settings.getAsBoolean("ignore_case", false); this.removeTrailing = settings.getAsBoolean("remove_trailing", true); this.stopWords = Analysis.parseWords( diff --git a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseTokenizerTokenizerFactory.java b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseTokenizerTokenizerFactory.java index 2545c9c7d94e..2b0a9bfe0034 100644 --- a/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseTokenizerTokenizerFactory.java +++ b/plugins/analysis-smartcn/src/main/java/org/elasticsearch/plugin/analysis/smartcn/SmartChineseTokenizerTokenizerFactory.java @@ -19,7 +19,7 @@ public class SmartChineseTokenizerTokenizerFactory extends AbstractTokenizerFactory { public SmartChineseTokenizerTokenizerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); } @Override diff --git a/plugins/analysis-stempel/src/main/java/org/elasticsearch/index/analysis/pl/PolishAnalyzerProvider.java b/plugins/analysis-stempel/src/main/java/org/elasticsearch/index/analysis/pl/PolishAnalyzerProvider.java index 68e7298473cd..73f42930e861 100644 --- a/plugins/analysis-stempel/src/main/java/org/elasticsearch/index/analysis/pl/PolishAnalyzerProvider.java +++ b/plugins/analysis-stempel/src/main/java/org/elasticsearch/index/analysis/pl/PolishAnalyzerProvider.java @@ -20,7 +20,7 @@ public class PolishAnalyzerProvider extends AbstractIndexAnalyzerProvider> getTokenFilters() { - return singletonMap( - "test_token_filter", - (indexSettings, environment, name, settings) -> new AbstractTokenFilterFactory(name, settings) { - @Override - public TokenStream create(TokenStream tokenStream) { - if (throwParsingError.get()) { - throw new MapperParsingException("simulate mapping parsing error"); - } - return tokenStream; + return singletonMap("test_token_filter", (indexSettings, environment, name, settings) -> new AbstractTokenFilterFactory(name) { + @Override + public TokenStream create(TokenStream tokenStream) { + if (throwParsingError.get()) { + throw new MapperParsingException("simulate mapping parsing error"); } + return tokenStream; } - ); + }); } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java index 36580ebda8ae..fc105d3d4fcd 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/fetch/subphase/highlight/HighlighterSearchIT.java @@ -3703,8 +3703,9 @@ public List getPreConfiguredTokenFilters() { @Override public Map>> getAnalyzers() { - return singletonMap("mock_whitespace", (indexSettings, environment, name, settings) -> { - return new AbstractIndexAnalyzerProvider(name, settings) { + return singletonMap( + "mock_whitespace", + (indexSettings, environment, name, settings) -> new AbstractIndexAnalyzerProvider(name) { MockAnalyzer instance = new MockAnalyzer(random(), MockTokenizer.WHITESPACE, false); @@ -3712,8 +3713,8 @@ public Map implements AnalyzerProvider { @@ -21,9 +20,8 @@ public abstract class AbstractIndexAnalyzerProvider implemen * * @param name The analyzer name */ - public AbstractIndexAnalyzerProvider(String name, Settings settings) { + public AbstractIndexAnalyzerProvider(String name) { this.name = name; - Analysis.checkForDeprecatedVersion(name, settings); } /** diff --git a/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenFilterFactory.java b/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenFilterFactory.java index 13b20a2d4249..7fbc16b54a37 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenFilterFactory.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenFilterFactory.java @@ -9,15 +9,12 @@ package org.elasticsearch.index.analysis; -import org.elasticsearch.common.settings.Settings; - public abstract class AbstractTokenFilterFactory implements TokenFilterFactory { private final String name; - public AbstractTokenFilterFactory(String name, Settings settings) { + public AbstractTokenFilterFactory(String name) { this.name = name; - Analysis.checkForDeprecatedVersion(name, settings); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenizerFactory.java b/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenizerFactory.java index 4b412003446a..2b34a45d31b0 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenizerFactory.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/AbstractTokenizerFactory.java @@ -9,15 +9,11 @@ package org.elasticsearch.index.analysis; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.IndexSettings; - public abstract class AbstractTokenizerFactory implements TokenizerFactory { private final String name; - public AbstractTokenizerFactory(IndexSettings indexSettings, Settings settings, String name) { - Analysis.checkForDeprecatedVersion(name, settings); + public AbstractTokenizerFactory(String name) { this.name = name; } diff --git a/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java b/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java index 462490a7fceb..505e39a9590e 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java @@ -50,8 +50,6 @@ import org.apache.lucene.analysis.util.CSVUtil; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.DeprecationCategory; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.synonyms.PagedResult; @@ -80,20 +78,8 @@ public class Analysis { - private static final DeprecationLogger DEPRECATION_LOGGER = DeprecationLogger.getLogger(Analysis.class); private static final Logger logger = LogManager.getLogger(Analysis.class); - public static void checkForDeprecatedVersion(String name, Settings settings) { - String sVersion = settings.get("version"); - if (sVersion != null) { - DEPRECATION_LOGGER.warn( - DeprecationCategory.ANALYSIS, - "analyzer.version", - "Setting [version] on analysis component [" + name + "] has no effect and is deprecated" - ); - } - } - public static CharArraySet parseStemExclusion(Settings settings, CharArraySet defaultStemExclusion) { String value = settings.get("stem_exclusion"); if ("_none_".equals(value)) { diff --git a/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzerProvider.java b/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzerProvider.java index ee6b2e5aaec9..1acf60e19925 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzerProvider.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/CustomAnalyzerProvider.java @@ -30,7 +30,7 @@ public class CustomAnalyzerProvider extends AbstractIndexAnalyzerProvider defaultAnalyzers() { .build(); private Analyzer createAnalyzerWithMode(AnalysisMode mode) { - TokenFilterFactory tokenFilter = new AbstractTokenFilterFactory("my_analyzer", Settings.EMPTY) { + TokenFilterFactory tokenFilter = new AbstractTokenFilterFactory("my_analyzer") { @Override public AnalysisMode getAnalysisMode() { return mode; diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java index a1b0aedaca2e..cf6941b84b79 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java @@ -113,7 +113,6 @@ public void testSimpleConfigurationJson() throws IOException { public void testSimpleConfigurationYaml() throws IOException { Settings settings = loadFromClasspath("/org/elasticsearch/index/analysis/test1.yml"); testSimpleConfiguration(settings); - assertWarnings("Setting [version] on analysis component [custom7] has no effect and is deprecated"); } private void testSimpleConfiguration(Settings settings) throws IOException { diff --git a/test/framework/src/main/java/org/elasticsearch/index/analysis/MyFilterTokenFilterFactory.java b/test/framework/src/main/java/org/elasticsearch/index/analysis/MyFilterTokenFilterFactory.java index e6871e720be1..df84a276dc55 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/analysis/MyFilterTokenFilterFactory.java +++ b/test/framework/src/main/java/org/elasticsearch/index/analysis/MyFilterTokenFilterFactory.java @@ -18,7 +18,7 @@ public class MyFilterTokenFilterFactory extends AbstractTokenFilterFactory { public MyFilterTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { - super(name, Settings.EMPTY); + super(name); } @Override diff --git a/test/framework/src/main/resources/org/elasticsearch/analysis/common/test1.yml b/test/framework/src/main/resources/org/elasticsearch/analysis/common/test1.yml index 095b27e0fa07..454b9bc8fc70 100644 --- a/test/framework/src/main/resources/org/elasticsearch/analysis/common/test1.yml +++ b/test/framework/src/main/resources/org/elasticsearch/analysis/common/test1.yml @@ -45,7 +45,6 @@ index : position_increment_gap: 256 custom7 : type : standard - version: 3.6 czechAnalyzerWithStemmer : tokenizer : standard filter : [lowercase, stop, czech_stem] diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlClassicTokenizerFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlClassicTokenizerFactory.java index 0ad3444884f1..e96c72b5d2f0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlClassicTokenizerFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlClassicTokenizerFactory.java @@ -20,7 +20,7 @@ public class MlClassicTokenizerFactory extends AbstractTokenizerFactory { public MlClassicTokenizerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlStandardTokenizerFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlStandardTokenizerFactory.java index 4ceaf4f1967c..c94816bac81d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlStandardTokenizerFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/categorization/MlStandardTokenizerFactory.java @@ -23,7 +23,7 @@ public class MlStandardTokenizerFactory extends AbstractTokenizerFactory { public MlStandardTokenizerFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { - super(indexSettings, settings, name); + super(name); } @Override From 795cd7e08968a16b4381632ab224c05537e7ea81 Mon Sep 17 00:00:00 2001 From: Aditya Singh <72691999+as1605@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:33:43 +0530 Subject: [PATCH 30/39] Fixed variable name in Zstd publish script (#118207) --- dev-tools/publish_zstd_binaries.sh | 4 ++-- docs/changelog/118207.yaml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/118207.yaml diff --git a/dev-tools/publish_zstd_binaries.sh b/dev-tools/publish_zstd_binaries.sh index d5be5c4aaec6..25d4aed6255b 100755 --- a/dev-tools/publish_zstd_binaries.sh +++ b/dev-tools/publish_zstd_binaries.sh @@ -79,8 +79,8 @@ build_linux_jar() { } echo 'Building Linux jars...' -LINUX_ARM_JAR=$(build_linux_jar "linux/amd64" "x86-64") -LINUX_X86_JAR=$(build_linux_jar "linux/arm64" "aarch64") +LINUX_ARM_JAR=$(build_linux_jar "linux/arm64" "aarch64") +LINUX_X86_JAR=$(build_linux_jar "linux/amd64" "x86-64") build_windows_jar() { ARTIFACT="$TEMP/zstd-$VERSION-windows-x86-64.jar" diff --git a/docs/changelog/118207.yaml b/docs/changelog/118207.yaml new file mode 100644 index 000000000000..989b3c0c5063 --- /dev/null +++ b/docs/changelog/118207.yaml @@ -0,0 +1 @@ +ixed variable name in Zstd publish script for linux platform From 9472489441b00d2460c7b47f52c7754891736e47 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Mon, 9 Dec 2024 22:36:16 +0100 Subject: [PATCH 31/39] Upgrade randomized runner to 2.8.2 (#118242) A recent bug we found has been fixed, so the upgrade pulls in the fix and this commit also removes the workaround we put in place. --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 15 ++++++++++----- .../test/AbstractMultiClustersTestCase.java | 7 ------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index aaf654a37dd2..ede1b392b8a4 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -35,7 +35,7 @@ commonscodec = 1.15 protobuf = 3.25.5 # test dependencies -randomizedrunner = 2.8.0 +randomizedrunner = 2.8.2 junit = 4.13.2 junit5 = 5.7.1 hamcrest = 2.1 diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9189d2a27f3f..33addef8aedd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -184,6 +184,11 @@ + + + + + @@ -4478,11 +4483,11 @@ - - - - - + + + + + diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractMultiClustersTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractMultiClustersTestCase.java index 7cd7bce4db18..b4f91f68b8bb 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractMultiClustersTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractMultiClustersTestCase.java @@ -9,8 +9,6 @@ package org.elasticsearch.test; -import com.carrotsearch.randomizedtesting.RandomizedTest; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.cluster.remote.RemoteInfoRequest; @@ -110,11 +108,6 @@ public final void startClusters() throws Exception { MockTransportService.TestPlugin.class, getTestTransportPlugin() ); - // We are going to initialize multiple clusters concurrently, but there is a race condition around the lazy initialization of test - // groups in GroupEvaluator across multiple threads. See https://github.com/randomizedtesting/randomizedtesting/issues/311. - // Calling isNightly before parallelizing is enough to work around that issue. - @SuppressWarnings("unused") - boolean nightly = RandomizedTest.isNightly(); runInParallel(clusterAliases.size(), i -> { String clusterAlias = clusterAliases.get(i); final String clusterName = clusterAlias.equals(LOCAL_CLUSTER) ? "main-cluster" : clusterAlias; From afd88cc5fa0e5b1e18279f7562d9830e1aa64e15 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Mon, 9 Dec 2024 13:58:09 -0800 Subject: [PATCH 32/39] Remove testing for 9.0 unsupported platforms (#118299) --- .buildkite/pipelines/periodic-packaging.template.yml | 5 ----- .buildkite/pipelines/periodic-packaging.yml | 5 ----- .buildkite/pipelines/periodic-platform-support.yml | 6 ------ .buildkite/pipelines/pull-request/packaging-tests-unix.yml | 5 ----- 4 files changed, 21 deletions(-) diff --git a/.buildkite/pipelines/periodic-packaging.template.yml b/.buildkite/pipelines/periodic-packaging.template.yml index 1a1e46d55f7a..aff0add62a2b 100644 --- a/.buildkite/pipelines/periodic-packaging.template.yml +++ b/.buildkite/pipelines/periodic-packaging.template.yml @@ -7,19 +7,14 @@ steps: matrix: setup: image: - - debian-11 - debian-12 - opensuse-leap-15 - - oraclelinux-7 - oraclelinux-8 - - sles-12 - sles-15 - - ubuntu-1804 - ubuntu-2004 - ubuntu-2204 - rocky-8 - rocky-9 - - rhel-7 - rhel-8 - rhel-9 - almalinux-8 diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml index c1b10a46c62a..9bcd61ac1273 100644 --- a/.buildkite/pipelines/periodic-packaging.yml +++ b/.buildkite/pipelines/periodic-packaging.yml @@ -8,19 +8,14 @@ steps: matrix: setup: image: - - debian-11 - debian-12 - opensuse-leap-15 - - oraclelinux-7 - oraclelinux-8 - - sles-12 - sles-15 - - ubuntu-1804 - ubuntu-2004 - ubuntu-2204 - rocky-8 - rocky-9 - - rhel-7 - rhel-8 - rhel-9 - almalinux-8 diff --git a/.buildkite/pipelines/periodic-platform-support.yml b/.buildkite/pipelines/periodic-platform-support.yml index 79e5a2e8dcdb..8bee3a78f831 100644 --- a/.buildkite/pipelines/periodic-platform-support.yml +++ b/.buildkite/pipelines/periodic-platform-support.yml @@ -7,19 +7,14 @@ steps: matrix: setup: image: - - debian-11 - debian-12 - opensuse-leap-15 - - oraclelinux-7 - oraclelinux-8 - - sles-12 - sles-15 - - ubuntu-1804 - ubuntu-2004 - ubuntu-2204 - rocky-8 - rocky-9 - - rhel-7 - rhel-8 - rhel-9 - almalinux-8 @@ -90,7 +85,6 @@ steps: setup: image: - amazonlinux-2023 - - amazonlinux-2 agents: provider: aws imagePrefix: elasticsearch-{{matrix.image}} diff --git a/.buildkite/pipelines/pull-request/packaging-tests-unix.yml b/.buildkite/pipelines/pull-request/packaging-tests-unix.yml index ffc1350aceab..6c7dadfd454e 100644 --- a/.buildkite/pipelines/pull-request/packaging-tests-unix.yml +++ b/.buildkite/pipelines/pull-request/packaging-tests-unix.yml @@ -10,19 +10,14 @@ steps: matrix: setup: image: - - debian-11 - debian-12 - opensuse-leap-15 - - oraclelinux-7 - oraclelinux-8 - - sles-12 - sles-15 - - ubuntu-1804 - ubuntu-2004 - ubuntu-2204 - rocky-8 - rocky-9 - - rhel-7 - rhel-8 - rhel-9 - almalinux-8 From 2be8b6744e4b5837c7221194f3f0b196c2d21855 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Mon, 9 Dec 2024 23:04:22 +0100 Subject: [PATCH 33/39] ESQL: Enable physical plan verification (#118114) This enables the physical plan verification. For it, a couple of changes needed to be applied/corrected: * AggregateMapper creates attributes with unique names; * AggregateExec's verification needs not consider ordinal attribute(s); * LookupJoinExec needs to merge attributes of same name at output, "winning" the right child; * ExchangeExec does no input referencing, since it only outputs all synthetic attributes, "sourced" from remote exchanges; * FieldExtractExec doesn't reference the attributes it "produces". --- docs/changelog/118114.yaml | 5 ++ .../xpack/esql/core/expression/Attribute.java | 5 +- .../optimizer/LocalPhysicalPlanOptimizer.java | 2 +- .../esql/optimizer/PhysicalVerifier.java | 15 +++- .../rules/PlanConsistencyChecker.java | 6 +- .../physical/local/InsertFieldExtraction.java | 17 +---- .../esql/plan/physical/AggregateExec.java | 21 ++++++ .../esql/plan/physical/ExchangeExec.java | 7 ++ .../esql/plan/physical/FieldExtractExec.java | 7 +- .../esql/plan/physical/LookupJoinExec.java | 6 +- .../AbstractPhysicalOperationProviders.java | 4 +- .../xpack/esql/planner/AggregateMapper.java | 62 ++++++++-------- .../LocalPhysicalPlanOptimizerTests.java | 10 +-- .../optimizer/PhysicalPlanOptimizerTests.java | 72 +++++++++++++++++-- 14 files changed, 164 insertions(+), 75 deletions(-) create mode 100644 docs/changelog/118114.yaml diff --git a/docs/changelog/118114.yaml b/docs/changelog/118114.yaml new file mode 100644 index 000000000000..1b7532d5df98 --- /dev/null +++ b/docs/changelog/118114.yaml @@ -0,0 +1,5 @@ +pr: 118114 +summary: Enable physical plan verification +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java index 53debedafc3d..829943d24514 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java @@ -49,8 +49,9 @@ public Attribute(Source source, String name, Nullability nullability, @Nullable this.nullability = nullability; } - public static String rawTemporaryName(String inner, String outer, String suffix) { - return SYNTHETIC_ATTRIBUTE_NAME_PREFIX + inner + "$" + outer + "$" + suffix; + public static String rawTemporaryName(String... parts) { + var name = String.join("$", parts); + return name.isEmpty() || name.startsWith(SYNTHETIC_ATTRIBUTE_NAME_PREFIX) ? name : SYNTHETIC_ATTRIBUTE_NAME_PREFIX + name; } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java index 48bafd8eef00..1eaade043658 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java @@ -57,7 +57,7 @@ protected List> batches() { } protected List> rules(boolean optimizeForEsSource) { - List> esSourceRules = new ArrayList<>(4); + List> esSourceRules = new ArrayList<>(6); esSourceRules.add(new ReplaceSourceAttributes()); if (optimizeForEsSource) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java index 8bd8aba01fd2..20528f8dc282 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java @@ -8,9 +8,11 @@ package org.elasticsearch.xpack.esql.optimizer; import org.elasticsearch.xpack.esql.common.Failure; +import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.optimizer.rules.PlanConsistencyChecker; +import org.elasticsearch.xpack.esql.plan.physical.AggregateExec; import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; @@ -31,10 +33,14 @@ private PhysicalVerifier() {} /** Verifies the physical plan. */ public Collection verify(PhysicalPlan plan) { Set failures = new LinkedHashSet<>(); + Failures depFailures = new Failures(); plan.forEachDown(p -> { - // FIXME: re-enable - // DEPENDENCY_CHECK.checkPlan(p, failures); + if (p instanceof AggregateExec agg) { + var exclude = Expressions.references(agg.ordinalAttributes()); + DEPENDENCY_CHECK.checkPlan(p, exclude, depFailures); + return; + } if (p instanceof FieldExtractExec fieldExtractExec) { Attribute sourceAttribute = fieldExtractExec.sourceAttribute(); if (sourceAttribute == null) { @@ -48,8 +54,13 @@ public Collection verify(PhysicalPlan plan) { ); } } + DEPENDENCY_CHECK.checkPlan(p, depFailures); }); + if (depFailures.hasFailures()) { + throw new IllegalStateException(depFailures.toString()); + } + return failures; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java index 30de8945a4c2..5101e3f73bfd 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java @@ -26,9 +26,13 @@ public class PlanConsistencyChecker

> { * {@link org.elasticsearch.xpack.esql.common.Failure Failure}s to the {@link Failures} object. */ public void checkPlan(P p, Failures failures) { + checkPlan(p, AttributeSet.EMPTY, failures); + } + + public void checkPlan(P p, AttributeSet exclude, Failures failures) { AttributeSet refs = p.references(); AttributeSet input = p.inputSet(); - AttributeSet missing = refs.subtract(input); + AttributeSet missing = refs.subtract(input).subtract(exclude); // TODO: for Joins, we should probably check if the required fields from the left child are actually in the left child, not // just any child (and analogously for the right child). if (missing.isEmpty() == false) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/InsertFieldExtraction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/InsertFieldExtraction.java index ed8851b64c27..61b1554fb71b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/InsertFieldExtraction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/InsertFieldExtraction.java @@ -11,7 +11,6 @@ import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute; -import org.elasticsearch.xpack.esql.expression.function.grouping.Categorize; import org.elasticsearch.xpack.esql.optimizer.rules.physical.ProjectAwayColumns; import org.elasticsearch.xpack.esql.plan.physical.AggregateExec; import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec; @@ -22,7 +21,6 @@ import java.util.ArrayList; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -54,18 +52,9 @@ public PhysicalPlan apply(PhysicalPlan plan) { * it loads the field lazily. If we have more than one field we need to * make sure the fields are loaded for the standard hash aggregator. */ - if (p instanceof AggregateExec agg && agg.groupings().size() == 1) { - // CATEGORIZE requires the standard hash aggregator as well. - if (agg.groupings().get(0).anyMatch(e -> e instanceof Categorize) == false) { - var leaves = new LinkedList<>(); - // TODO: this seems out of place - agg.aggregates() - .stream() - .filter(a -> agg.groupings().contains(a) == false) - .forEach(a -> leaves.addAll(a.collectLeaves())); - var remove = agg.groupings().stream().filter(g -> leaves.contains(g) == false).toList(); - missing.removeAll(Expressions.references(remove)); - } + if (p instanceof AggregateExec agg) { + var ordinalAttributes = agg.ordinalAttributes(); + missing.removeAll(Expressions.references(ordinalAttributes)); } // add extractor diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java index 891d03c571b2..35f45250ed27 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java @@ -18,10 +18,13 @@ import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.function.grouping.Categorize; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.plan.logical.Aggregate; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -184,6 +187,24 @@ protected AttributeSet computeReferences() { return mode.isInputPartial() ? new AttributeSet(intermediateAttributes) : Aggregate.computeReferences(aggregates, groupings); } + /** Returns the attributes that can be loaded from ordinals -- no explicit extraction is needed */ + public List ordinalAttributes() { + List orginalAttributs = new ArrayList<>(groupings.size()); + // Ordinals can be leveraged just for a single grouping. If there are multiple groupings, fields need to be laoded for the + // hash aggregator. + // CATEGORIZE requires the standard hash aggregator as well. + if (groupings().size() == 1 && groupings.get(0).anyMatch(e -> e instanceof Categorize) == false) { + var leaves = new HashSet<>(); + aggregates.stream().filter(a -> groupings.contains(a) == false).forEach(a -> leaves.addAll(a.collectLeaves())); + groupings.forEach(g -> { + if (leaves.contains(g) == false) { + orginalAttributs.add((Attribute) g); + } + }); + } + return orginalAttributs; + } + @Override public int hashCode() { return Objects.hash(groupings, aggregates, mode, intermediateAttributes, estimatedRowSize, child()); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/ExchangeExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/ExchangeExec.java index 5530b3ea54d3..d1d834b71047 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/ExchangeExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/ExchangeExec.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.esql.core.expression.Attribute; +import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; @@ -72,6 +73,12 @@ public boolean inBetweenAggs() { return inBetweenAggs; } + @Override + protected AttributeSet computeReferences() { + // ExchangeExec does no input referencing, it only outputs all synthetic attributes, "sourced" from remote exchanges. + return AttributeSet.EMPTY; + } + @Override public UnaryExec replaceChild(PhysicalPlan newChild) { return new ExchangeExec(source(), output, inBetweenAggs, newChild); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/FieldExtractExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/FieldExtractExec.java index 35c6e4846bd8..ec996c5c8406 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/FieldExtractExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/FieldExtractExec.java @@ -89,12 +89,7 @@ public static Attribute extractSourceAttributesFrom(PhysicalPlan plan) { @Override protected AttributeSet computeReferences() { - AttributeSet required = new AttributeSet(docValuesAttributes); - - required.add(sourceAttribute); - required.addAll(attributesToExtract); - - return required; + return sourceAttribute != null ? new AttributeSet(sourceAttribute) : AttributeSet.EMPTY; } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java index 8b1cc047309e..26fd12447e66 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java @@ -93,9 +93,9 @@ public List addedFields() { public List output() { if (lazyOutput == null) { lazyOutput = new ArrayList<>(left().output()); - for (Attribute attr : addedFields) { - lazyOutput.add(attr); - } + var addedFieldsNames = addedFields.stream().map(Attribute::name).toList(); + lazyOutput.removeIf(a -> addedFieldsNames.contains(a.name())); + lazyOutput.addAll(addedFields); } return lazyOutput; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AbstractPhysicalOperationProviders.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AbstractPhysicalOperationProviders.java index 35aba7665ec8..57ba1c8016fe 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AbstractPhysicalOperationProviders.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AbstractPhysicalOperationProviders.java @@ -297,9 +297,9 @@ private void aggregatesToFactory( // coordinator/exchange phase else if (mode == AggregatorMode.FINAL || mode == AggregatorMode.INTERMEDIATE) { if (grouping) { - sourceAttr = aggregateMapper.mapGrouping(aggregateFunction); + sourceAttr = aggregateMapper.mapGrouping(ne); } else { - sourceAttr = aggregateMapper.mapNonGrouping(aggregateFunction); + sourceAttr = aggregateMapper.mapNonGrouping(ne); } } else { throw new EsqlIllegalArgumentException("illegal aggregation mode"); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java index 18bbfdf485a8..1f55e293b8e7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java @@ -13,6 +13,7 @@ import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; import org.elasticsearch.xpack.esql.core.expression.Alias; +import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.AttributeMap; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; @@ -91,7 +92,7 @@ final class AggregateMapper { private record AggDef(Class aggClazz, String type, String extra, boolean grouping) {} /** Map of AggDef types to intermediate named expressions. */ - private static final Map> mapper = AGG_FUNCTIONS.stream() + private static final Map> MAPPER = AGG_FUNCTIONS.stream() .flatMap(AggregateMapper::typeAndNames) .flatMap(AggregateMapper::groupingAndNonGrouping) .collect(Collectors.toUnmodifiableMap(aggDef -> aggDef, AggregateMapper::lookupIntermediateState)); @@ -103,50 +104,57 @@ private record AggDef(Class aggClazz, String type, String extra, boolean grou cache = new HashMap<>(); } - public List mapNonGrouping(List aggregates) { + public List mapNonGrouping(List aggregates) { return doMapping(aggregates, false); } - public List mapNonGrouping(Expression aggregate) { + public List mapNonGrouping(NamedExpression aggregate) { return map(aggregate, false).toList(); } - public List mapGrouping(List aggregates) { + public List mapGrouping(List aggregates) { return doMapping(aggregates, true); } - private List doMapping(List aggregates, boolean grouping) { + private List doMapping(List aggregates, boolean grouping) { AttributeMap attrToExpressions = new AttributeMap<>(); - aggregates.stream().flatMap(agg -> map(agg, grouping)).forEach(ne -> attrToExpressions.put(ne.toAttribute(), ne)); + aggregates.stream().flatMap(ne -> map(ne, grouping)).forEach(ne -> attrToExpressions.put(ne.toAttribute(), ne)); return attrToExpressions.values().stream().toList(); } - public List mapGrouping(Expression aggregate) { + public List mapGrouping(NamedExpression aggregate) { return map(aggregate, true).toList(); } - private Stream map(Expression aggregate, boolean grouping) { - return cache.computeIfAbsent(Alias.unwrap(aggregate), aggKey -> computeEntryForAgg(aggKey, grouping)).stream(); + private Stream map(NamedExpression ne, boolean grouping) { + return cache.computeIfAbsent(Alias.unwrap(ne), aggKey -> computeEntryForAgg(ne.name(), aggKey, grouping)).stream(); } - private static List computeEntryForAgg(Expression aggregate, boolean grouping) { - var aggDef = aggDefOrNull(aggregate, grouping); - if (aggDef != null) { - var is = getNonNull(aggDef); - var exp = isToNE(is).toList(); - return exp; + private static List computeEntryForAgg(String aggAlias, Expression aggregate, boolean grouping) { + if (aggregate instanceof AggregateFunction aggregateFunction) { + return entryForAgg(aggAlias, aggregateFunction, grouping); } if (aggregate instanceof FieldAttribute || aggregate instanceof MetadataAttribute || aggregate instanceof ReferenceAttribute) { - // This condition is a little pedantic, but do we expected other expressions here? if so, then add them + // This condition is a little pedantic, but do we expect other expressions here? if so, then add them return List.of(); - } else { - throw new EsqlIllegalArgumentException("unknown agg: " + aggregate.getClass() + ": " + aggregate); } + throw new EsqlIllegalArgumentException("unknown agg: " + aggregate.getClass() + ": " + aggregate); + } + + private static List entryForAgg(String aggAlias, AggregateFunction aggregateFunction, boolean grouping) { + var aggDef = new AggDef( + aggregateFunction.getClass(), + dataTypeToString(aggregateFunction.field().dataType(), aggregateFunction.getClass()), + aggregateFunction instanceof SpatialCentroid ? "SourceValues" : "", + grouping + ); + var is = getNonNull(aggDef); + return isToNE(is, aggAlias).toList(); } /** Gets the agg from the mapper - wrapper around map::get for more informative failure.*/ private static List getNonNull(AggDef aggDef) { - var l = mapper.get(aggDef); + var l = MAPPER.get(aggDef); if (l == null) { throw new EsqlIllegalArgumentException("Cannot find intermediate state for: " + aggDef); } @@ -199,18 +207,6 @@ private static Stream groupingAndNonGrouping(Tuple, Tuple lookupIntermediateState(AggDef aggDef) { try { @@ -257,7 +253,7 @@ private static String determinePackageName(Class clazz) { } /** Maps intermediate state description to named expressions. */ - private static Stream isToNE(List intermediateStateDescs) { + private static Stream isToNE(List intermediateStateDescs, String aggAlias) { return intermediateStateDescs.stream().map(is -> { final DataType dataType; if (Strings.isEmpty(is.dataType())) { @@ -265,7 +261,7 @@ private static Stream isToNE(List interm } else { dataType = DataType.fromEs(is.dataType()); } - return new ReferenceAttribute(Source.EMPTY, is.name(), dataType); + return new ReferenceAttribute(Source.EMPTY, Attribute.rawTemporaryName(aggAlias, is.name()), dataType); }); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 6123a464378f..879a41361520 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -255,7 +255,7 @@ public void testCountFieldWithEval() { var esStatsQuery = as(exg.child(), EsStatsQueryExec.class); assertThat(esStatsQuery.limit(), is(nullValue())); - assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen")); + assertThat(Expressions.names(esStatsQuery.output()), contains("$$c$count", "$$c$seen")); var stat = as(esStatsQuery.stats().get(0), Stat.class); assertThat(stat.query(), is(QueryBuilders.existsQuery("salary"))); } @@ -276,7 +276,7 @@ public void testCountOneFieldWithFilter() { var exchange = as(agg.child(), ExchangeExec.class); var esStatsQuery = as(exchange.child(), EsStatsQueryExec.class); assertThat(esStatsQuery.limit(), is(nullValue())); - assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen")); + assertThat(Expressions.names(esStatsQuery.output()), contains("$$c$count", "$$c$seen")); var stat = as(esStatsQuery.stats().get(0), Stat.class); Source source = new Source(2, 8, "salary > 1000"); var exists = QueryBuilders.existsQuery("salary"); @@ -386,7 +386,7 @@ public void testAnotherCountAllWithFilter() { var exchange = as(agg.child(), ExchangeExec.class); var esStatsQuery = as(exchange.child(), EsStatsQueryExec.class); assertThat(esStatsQuery.limit(), is(nullValue())); - assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen")); + assertThat(Expressions.names(esStatsQuery.output()), contains("$$c$count", "$$c$seen")); var source = ((SingleValueQuery.Builder) esStatsQuery.query()).source(); var expected = wrapWithSingleQuery(query, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", source); assertThat(expected.toString(), is(esStatsQuery.query().toString())); @@ -997,7 +997,7 @@ public boolean exists(String field) { var exchange = as(agg.child(), ExchangeExec.class); assertThat(exchange.inBetweenAggs(), is(true)); var localSource = as(exchange.child(), LocalSourceExec.class); - assertThat(Expressions.names(localSource.output()), contains("count", "seen")); + assertThat(Expressions.names(localSource.output()), contains("$$c$count", "$$c$seen")); } /** @@ -1152,7 +1152,7 @@ public void testIsNotNull_TextField_Pushdown_WithCount() { var exg = as(agg.child(), ExchangeExec.class); var esStatsQuery = as(exg.child(), EsStatsQueryExec.class); assertThat(esStatsQuery.limit(), is(nullValue())); - assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen")); + assertThat(Expressions.names(esStatsQuery.output()), contains("$$c$count", "$$c$seen")); var stat = as(esStatsQuery.stats().get(0), Stat.class); assertThat(stat.query(), is(QueryBuilders.existsQuery("job"))); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index c35f01e9fe77..9682bb1c8b07 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.compute.aggregation.AggregatorMode; import org.elasticsearch.core.Tuple; import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Polygon; @@ -127,6 +128,7 @@ import org.elasticsearch.xpack.esql.stats.SearchStats; import org.junit.Before; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -2286,6 +2288,58 @@ public void testFieldExtractWithoutSourceAttributes() { ); } + public void testVerifierOnMissingReferences() { + var plan = physicalPlan(""" + from test + | stats s = sum(salary) by emp_no + | where emp_no > 10 + """); + + plan = plan.transformUp( + AggregateExec.class, + a -> new AggregateExec( + a.source(), + a.child(), + a.groupings(), + List.of(), // remove the aggs (and thus the groupings) entirely + a.getMode(), + a.intermediateAttributes(), + a.estimatedRowSize() + ) + ); + final var finalPlan = plan; + var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(finalPlan)); + assertThat(e.getMessage(), containsString(" > 10[INTEGER]]] optimized incorrectly due to missing references [emp_no{f}#")); + } + + public void testVerifierOnDuplicateOutputAttributes() { + var plan = physicalPlan(""" + from test + | stats s = sum(salary) by emp_no + | where emp_no > 10 + """); + + plan = plan.transformUp(AggregateExec.class, a -> { + List intermediates = new ArrayList<>(a.intermediateAttributes()); + intermediates.add(intermediates.get(0)); + return new AggregateExec( + a.source(), + a.child(), + a.groupings(), + a.aggregates(), + AggregatorMode.INTERMEDIATE, // FINAL would deduplicate aggregates() + intermediates, + a.estimatedRowSize() + ); + }); + final var finalPlan = plan; + var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(finalPlan)); + assertThat( + e.getMessage(), + containsString("Plan [LimitExec[1000[INTEGER]]] optimized incorrectly due to duplicate output attribute emp_no{f}#") + ); + } + public void testProjectAwayColumns() { var rule = new ProjectAwayColumns(); @@ -2557,7 +2611,7 @@ public boolean exists(String field) { var exchange = asRemoteExchange(aggregate.child()); var localSourceExec = as(exchange.child(), LocalSourceExec.class); - assertThat(Expressions.names(localSourceExec.output()), contains("languages", "min", "seen")); + assertThat(Expressions.names(localSourceExec.output()), contains("languages", "$$m$min", "$$m$seen")); } /** @@ -2593,9 +2647,9 @@ public void testPartialAggFoldingOutput() { var limit = as(optimized, LimitExec.class); var agg = as(limit.child(), AggregateExec.class); var exchange = as(agg.child(), ExchangeExec.class); - assertThat(Expressions.names(exchange.output()), contains("count", "seen")); + assertThat(Expressions.names(exchange.output()), contains("$$c$count", "$$c$seen")); var source = as(exchange.child(), LocalSourceExec.class); - assertThat(Expressions.names(source.output()), contains("count", "seen")); + assertThat(Expressions.names(source.output()), contains("$$c$count", "$$c$seen")); } /** @@ -2627,7 +2681,7 @@ public void testGlobalAggFoldingOutput() { var aggFinal = as(limit.child(), AggregateExec.class); var aggPartial = as(aggFinal.child(), AggregateExec.class); // The partial aggregation's output is determined via AbstractPhysicalOperationProviders.intermediateAttributes() - assertThat(Expressions.names(aggPartial.output()), contains("count", "seen")); + assertThat(Expressions.names(aggPartial.output()), contains("$$c$count", "$$c$seen")); limit = as(aggPartial.child(), LimitExec.class); var exchange = as(limit.child(), ExchangeExec.class); var project = as(exchange.child(), ProjectExec.class); @@ -2665,9 +2719,15 @@ public void testPartialAggFoldingOutputForSyntheticAgg() { var aggFinal = as(limit.child(), AggregateExec.class); assertThat(aggFinal.output(), hasSize(2)); var exchange = as(aggFinal.child(), ExchangeExec.class); - assertThat(Expressions.names(exchange.output()), contains("sum", "seen", "count", "seen")); + assertThat( + Expressions.names(exchange.output()), + contains("$$SUM$a$0$sum", "$$SUM$a$0$seen", "$$COUNT$a$1$count", "$$COUNT$a$1$seen") + ); var source = as(exchange.child(), LocalSourceExec.class); - assertThat(Expressions.names(source.output()), contains("sum", "seen", "count", "seen")); + assertThat( + Expressions.names(source.output()), + contains("$$SUM$a$0$sum", "$$SUM$a$0$seen", "$$COUNT$a$1$count", "$$COUNT$a$1$seen") + ); } /** From f78ddaedcb0296f857d334a7aad41ea75598c6b3 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Mon, 9 Dec 2024 14:07:16 -0800 Subject: [PATCH 34/39] Delete changelog entry --- docs/changelog/118207.yaml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/changelog/118207.yaml diff --git a/docs/changelog/118207.yaml b/docs/changelog/118207.yaml deleted file mode 100644 index 989b3c0c5063..000000000000 --- a/docs/changelog/118207.yaml +++ /dev/null @@ -1 +0,0 @@ -ixed variable name in Zstd publish script for linux platform From 4f253dcfae773a0d0e0f0a7b7798bd066ac4a89d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:24:20 +1100 Subject: [PATCH 35/39] Mute org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormatTests testSingleVectorCase #118306 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index d356dd2f791d..05831f3fb04b 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -291,6 +291,9 @@ tests: - class: org.elasticsearch.xpack.test.rest.XPackRestIT method: test {p0=migrate/10_reindex/Test Reindex With Nonexistent Data Stream} issue: https://github.com/elastic/elasticsearch/issues/118274 +- class: org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormatTests + method: testSingleVectorCase + issue: https://github.com/elastic/elasticsearch/issues/118306 # Examples: # From e52d0c9ae4b9584e44dd5b1bbbee142c11999d1c Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:44:15 +1100 Subject: [PATCH 36/39] Mute org.elasticsearch.action.search.SearchQueryThenFetchAsyncActionTests testBottomFieldSort #118214 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 05831f3fb04b..a57f79e598fe 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -294,6 +294,9 @@ tests: - class: org.elasticsearch.index.codec.vectors.es818.ES818HnswBinaryQuantizedVectorsFormatTests method: testSingleVectorCase issue: https://github.com/elastic/elasticsearch/issues/118306 +- class: org.elasticsearch.action.search.SearchQueryThenFetchAsyncActionTests + method: testBottomFieldSort + issue: https://github.com/elastic/elasticsearch/issues/118214 # Examples: # From 78620c6c1aa97030a024921b17b4dc2f0881e69d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:26:11 +1100 Subject: [PATCH 37/39] Mute org.elasticsearch.xpack.esql.action.CrossClustersEnrichIT testTopNThenEnrichRemote #118307 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index a57f79e598fe..12b7ea964969 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -297,6 +297,9 @@ tests: - class: org.elasticsearch.action.search.SearchQueryThenFetchAsyncActionTests method: testBottomFieldSort issue: https://github.com/elastic/elasticsearch/issues/118214 +- class: org.elasticsearch.xpack.esql.action.CrossClustersEnrichIT + method: testTopNThenEnrichRemote + issue: https://github.com/elastic/elasticsearch/issues/118307 # Examples: # From 34b7e60f7589f0fd01b04a5cf28a088a254ad295 Mon Sep 17 00:00:00 2001 From: James Baiera Date: Tue, 10 Dec 2024 01:32:56 -0500 Subject: [PATCH 38/39] Re-add ResolvedExpression wrapper (#118174) This PR reapplies #114592 along with an update to remove the performance regression introduced with the original change. --- .../TransportClusterSearchShardsAction.java | 3 +- .../indices/resolve/ResolveIndexAction.java | 9 +- .../query/TransportValidateQueryAction.java | 3 +- .../explain/TransportExplainAction.java | 3 +- .../action/search/TransportSearchAction.java | 24 ++- .../search/TransportSearchShardsAction.java | 6 +- .../metadata/IndexNameExpressionResolver.java | 140 ++++++++++------ .../elasticsearch/indices/IndicesService.java | 3 +- .../elasticsearch/search/SearchService.java | 3 +- .../indices/resolve/ResolveIndexTests.java | 15 +- .../IndexNameExpressionResolverTests.java | 74 ++++++--- .../WildcardExpressionResolverTests.java | 152 +++++++++++------- .../indices/IndicesServiceTests.java | 34 ++-- 13 files changed, 305 insertions(+), 164 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java index 9ffef1f178f4..b855f2cee761 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.GroupShardsIterator; import org.elasticsearch.cluster.routing.ShardIterator; @@ -84,7 +85,7 @@ protected void masterOperation( String[] concreteIndices = indexNameExpressionResolver.concreteIndexNames(clusterState, request); Map> routingMap = indexNameExpressionResolver.resolveSearchRouting(state, request.routing(), request.indices()); Map indicesAndFilters = new HashMap<>(); - Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, request.indices()); + Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, request.indices()); for (String index : concreteIndices) { final AliasFilter aliasFilter = indicesService.buildAliasFilter(clusterState, index, indicesAndAliases); final String[] aliases = indexNameExpressionResolver.indexAliases( diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java index 5c5c71bc002b..f5c100b7884b 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexAction.java @@ -25,6 +25,7 @@ import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; @@ -565,8 +566,8 @@ static void resolveIndices( if (names.length == 1 && (Metadata.ALL.equals(names[0]) || Regex.isMatchAllPattern(names[0]))) { names = new String[] { "**" }; } - Set resolvedIndexAbstractions = resolver.resolveExpressions(clusterState, indicesOptions, true, names); - for (String s : resolvedIndexAbstractions) { + Set resolvedIndexAbstractions = resolver.resolveExpressions(clusterState, indicesOptions, true, names); + for (ResolvedExpression s : resolvedIndexAbstractions) { enrichIndexAbstraction(clusterState, s, indices, aliases, dataStreams); } indices.sort(Comparator.comparing(ResolvedIndexAbstraction::getName)); @@ -597,12 +598,12 @@ private static void mergeResults( private static void enrichIndexAbstraction( ClusterState clusterState, - String indexAbstraction, + ResolvedExpression indexAbstraction, List indices, List aliases, List dataStreams ) { - IndexAbstraction ia = clusterState.metadata().getIndicesLookup().get(indexAbstraction); + IndexAbstraction ia = clusterState.metadata().getIndicesLookup().get(indexAbstraction.resource()); if (ia != null) { switch (ia.getType()) { case CONCRETE_INDEX -> { diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java index 4e9830fe0d14..e01f36471267 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java @@ -21,6 +21,7 @@ import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.routing.GroupShardsIterator; import org.elasticsearch.cluster.routing.ShardIterator; import org.elasticsearch.cluster.routing.ShardRouting; @@ -133,7 +134,7 @@ protected void doExecute(Task task, ValidateQueryRequest request, ActionListener @Override protected ShardValidateQueryRequest newShardRequest(int numShards, ShardRouting shard, ValidateQueryRequest request) { final ClusterState clusterState = clusterService.state(); - final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, request.indices()); + final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, request.indices()); final AliasFilter aliasFilter = searchService.buildAliasFilter(clusterState, shard.getIndexName(), indicesAndAliases); return new ShardValidateQueryRequest(shard.shardId(), aliasFilter, request); } diff --git a/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java b/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java index 9c82d032014f..84c6df7b8a66 100644 --- a/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java +++ b/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.action.support.single.shard.TransportSingleShardAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.routing.ShardIterator; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.Writeable; @@ -109,7 +110,7 @@ protected boolean resolveIndex(ExplainRequest request) { @Override protected void resolveRequest(ClusterState state, InternalRequest request) { - final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(state, request.request().index()); + final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(state, request.request().index()); final AliasFilter aliasFilter = searchService.buildAliasFilter(state, request.concreteIndex(), indicesAndAliases); request.request().filteringAlias(aliasFilter); } diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java index 5d1fb46a53ce..ae27406bf396 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -38,6 +38,7 @@ import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.GroupShardsIterator; @@ -111,6 +112,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.LongSupplier; +import java.util.stream.Collectors; import static org.elasticsearch.action.search.SearchType.DFS_QUERY_THEN_FETCH; import static org.elasticsearch.action.search.SearchType.QUERY_THEN_FETCH; @@ -207,7 +209,7 @@ public TransportSearchAction( private Map buildPerIndexOriginalIndices( ClusterState clusterState, - Set indicesAndAliases, + Set indicesAndAliases, String[] indices, IndicesOptions indicesOptions ) { @@ -215,6 +217,9 @@ private Map buildPerIndexOriginalIndices( var blocks = clusterState.blocks(); // optimization: mostly we do not have any blocks so there's no point in the expensive per-index checking boolean hasBlocks = blocks.global().isEmpty() == false || blocks.indices().isEmpty() == false; + // Get a distinct set of index abstraction names present from the resolved expressions to help with the reverse resolution from + // concrete index to the expression that produced it. + Set indicesAndAliasesResources = indicesAndAliases.stream().map(ResolvedExpression::resource).collect(Collectors.toSet()); for (String index : indices) { if (hasBlocks) { blocks.indexBlockedRaiseException(ClusterBlockLevel.READ, index); @@ -231,8 +236,8 @@ private Map buildPerIndexOriginalIndices( String[] finalIndices = Strings.EMPTY_ARRAY; if (aliases == null || aliases.length == 0 - || indicesAndAliases.contains(index) - || hasDataStreamRef(clusterState, indicesAndAliases, index)) { + || indicesAndAliasesResources.contains(index) + || hasDataStreamRef(clusterState, indicesAndAliasesResources, index)) { finalIndices = new String[] { index }; } if (aliases != null) { @@ -251,7 +256,11 @@ private static boolean hasDataStreamRef(ClusterState clusterState, Set i return indicesAndAliases.contains(ret.getParentDataStream().getName()); } - Map buildIndexAliasFilters(ClusterState clusterState, Set indicesAndAliases, Index[] concreteIndices) { + Map buildIndexAliasFilters( + ClusterState clusterState, + Set indicesAndAliases, + Index[] concreteIndices + ) { final Map aliasFilterMap = new HashMap<>(); for (Index index : concreteIndices) { clusterState.blocks().indexBlockedRaiseException(ClusterBlockLevel.READ, index.getName()); @@ -1236,7 +1245,10 @@ private void executeSearch( } else { final Index[] indices = resolvedIndices.getConcreteLocalIndices(); concreteLocalIndices = Arrays.stream(indices).map(Index::getName).toArray(String[]::new); - final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, searchRequest.indices()); + final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions( + clusterState, + searchRequest.indices() + ); aliasFilter = buildIndexAliasFilters(clusterState, indicesAndAliases, indices); aliasFilter.putAll(remoteAliasMap); localShardIterators = getLocalShardsIterator( @@ -1835,7 +1847,7 @@ List getLocalShardsIterator( ClusterState clusterState, SearchRequest searchRequest, String clusterAlias, - Set indicesAndAliases, + Set indicesAndAliases, String[] concreteIndices ) { var routingMap = indexNameExpressionResolver.resolveSearchRouting(clusterState, searchRequest.routing(), searchRequest.indices()); diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java index d8b57972d604..614a3e9cf22a 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.routing.GroupShardsIterator; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.index.Index; @@ -127,7 +128,10 @@ public void searchShards(Task task, SearchShardsRequest searchShardsRequest, Act searchService.getRewriteContext(timeProvider::absoluteStartMillis, resolvedIndices, null), listener.delegateFailureAndWrap((delegate, searchRequest) -> { Index[] concreteIndices = resolvedIndices.getConcreteLocalIndices(); - final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions(clusterState, searchRequest.indices()); + final Set indicesAndAliases = indexNameExpressionResolver.resolveExpressions( + clusterState, + searchRequest.indices() + ); final Map aliasFilters = transportSearchAction.buildIndexAliasFilters( clusterState, indicesAndAliases, diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 279243eeff7c..e7914d812e05 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -80,6 +80,13 @@ public IndexNameExpressionResolver(ThreadContext threadContext, SystemIndices sy this.systemIndices = Objects.requireNonNull(systemIndices, "System Indices must not be null"); } + /** + * This represents a resolved expression in the form of the name of a resource in the cluster. + * Soon it will facilitate an index component selector, which will define which part of the resource the expression is targeting. + * @param resource the name of a resource that an expression refers to. + */ + public record ResolvedExpression(String resource) {} + /** * Same as {@link #concreteIndexNames(ClusterState, IndicesOptions, String...)}, but the index expressions and options * are encapsulated in the specified request. @@ -197,8 +204,9 @@ public List dataStreamNames(ClusterState state, IndicesOptions options, getSystemIndexAccessPredicate(), getNetNewSystemIndexPredicate() ); - final Collection expressions = resolveExpressionsToResources(context, indexExpressions); + final Collection expressions = resolveExpressionsToResources(context, indexExpressions); return expressions.stream() + .map(ResolvedExpression::resource) .map(x -> state.metadata().getIndicesLookup().get(x)) .filter(Objects::nonNull) .filter(ia -> ia.getType() == Type.DATA_STREAM) @@ -227,10 +235,11 @@ public IndexAbstraction resolveWriteIndexAbstraction(ClusterState state, DocWrit getNetNewSystemIndexPredicate() ); - final Collection expressions = resolveExpressionsToResources(context, request.index()); + final Collection expressions = resolveExpressionsToResources(context, request.index()); if (expressions.size() == 1) { - IndexAbstraction ia = state.metadata().getIndicesLookup().get(expressions.iterator().next()); + ResolvedExpression resolvedExpression = expressions.iterator().next(); + IndexAbstraction ia = state.metadata().getIndicesLookup().get(resolvedExpression.resource()); if (ia.getType() == Type.ALIAS) { Index writeIndex = ia.getWriteIndex(); if (writeIndex == null) { @@ -257,7 +266,7 @@ public IndexAbstraction resolveWriteIndexAbstraction(ClusterState state, DocWrit * If {@param preserveDataStreams} is {@code true}, data streams that are covered by the wildcards from the * {@param expressions} are returned as-is, without expanding them further to their respective backing indices. */ - protected static Collection resolveExpressionsToResources(Context context, String... expressions) { + protected static Collection resolveExpressionsToResources(Context context, String... expressions) { // If we do not expand wildcards, then empty or _all expression result in an empty list boolean expandWildcards = context.getOptions().expandWildcardExpressions(); if (expandWildcards == false) { @@ -275,7 +284,7 @@ protected static Collection resolveExpressionsToResources(Context contex } // Using ArrayList when we know we do not have wildcards is an optimisation, given that one expression result in 0 or 1 resources. - Collection resources = expandWildcards && WildcardExpressionResolver.hasWildcards(expressions) + Collection resources = expandWildcards && WildcardExpressionResolver.hasWildcards(expressions) ? new LinkedHashSet<>() : new ArrayList<>(expressions.length); boolean wildcardSeen = false; @@ -297,7 +306,7 @@ protected static Collection resolveExpressionsToResources(Context contex wildcardSeen |= isWildcard; if (isWildcard) { - Set matchingResources = WildcardExpressionResolver.matchWildcardToResources(context, baseExpression); + Set matchingResources = WildcardExpressionResolver.matchWildcardToResources(context, baseExpression); if (context.getOptions().allowNoIndices() == false && matchingResources.isEmpty()) { throw notFoundException(baseExpression); @@ -310,9 +319,9 @@ protected static Collection resolveExpressionsToResources(Context contex } } else { if (isExclusion) { - resources.remove(baseExpression); + resources.remove(new ResolvedExpression(baseExpression)); } else if (ensureAliasOrIndexExists(context, baseExpression)) { - resources.add(baseExpression); + resources.add(new ResolvedExpression(baseExpression)); } } } @@ -428,12 +437,12 @@ String[] concreteIndexNames(Context context, String... indexExpressions) { } Index[] concreteIndices(Context context, String... indexExpressions) { - final Collection expressions = resolveExpressionsToResources(context, indexExpressions); + final Collection expressions = resolveExpressionsToResources(context, indexExpressions); final Set concreteIndicesResult = Sets.newLinkedHashSetWithExpectedSize(expressions.size()); final Map indicesLookup = context.getState().metadata().getIndicesLookup(); - for (String expression : expressions) { - final IndexAbstraction indexAbstraction = indicesLookup.get(expression); + for (ResolvedExpression expression : expressions) { + final IndexAbstraction indexAbstraction = indicesLookup.get(expression.resource()); assert indexAbstraction != null; if (indexAbstraction.getType() == Type.ALIAS && context.isResolveToWriteIndex()) { Index writeIndex = indexAbstraction.getWriteIndex(); @@ -467,7 +476,7 @@ Index[] concreteIndices(Context context, String... indexExpressions) { throw new IllegalArgumentException( indexAbstraction.getType().getDisplayName() + " [" - + expression + + expression.resource() + "] has more than one index associated with it " + Arrays.toString(indexNames) + ", can't execute a single index op" @@ -682,7 +691,7 @@ public Index concreteSingleIndex(ClusterState state, IndicesRequest request) { * Utility method that allows to resolve an index expression to its corresponding single write index. * * @param state the cluster state containing all the data to resolve to expression to a concrete index - * @param request The request that defines how the an alias or an index need to be resolved to a concrete index + * @param request The request that defines how an alias or an index need to be resolved to a concrete index * and the expression that can be resolved to an alias or an index name. * @throws IllegalArgumentException if the index resolution does not lead to an index, or leads to more than one index * @return the write index obtained as a result of the index resolution @@ -759,7 +768,7 @@ public boolean hasIndexAbstraction(String indexAbstraction, ClusterState state) /** * Resolve an array of expressions to the set of indices and aliases that these expressions match. */ - public Set resolveExpressions(ClusterState state, String... expressions) { + public Set resolveExpressions(ClusterState state, String... expressions) { return resolveExpressions(state, IndicesOptions.lenientExpandOpen(), false, expressions); } @@ -768,7 +777,7 @@ public Set resolveExpressions(ClusterState state, String... expressions) * If {@param preserveDataStreams} is {@code true}, datastreams that are covered by the wildcards from the * {@param expressions} are returned as-is, without expanding them further to their respective backing indices. */ - public Set resolveExpressions( + public Set resolveExpressions( ClusterState state, IndicesOptions indicesOptions, boolean preserveDataStreams, @@ -786,10 +795,10 @@ public Set resolveExpressions( getNetNewSystemIndexPredicate() ); // unmodifiable without creating a new collection as it might contain many items - Collection resolved = resolveExpressionsToResources(context, expressions); - if (resolved instanceof Set) { + Collection resolved = resolveExpressionsToResources(context, expressions); + if (resolved instanceof Set) { // unmodifiable without creating a new collection as it might contain many items - return Collections.unmodifiableSet((Set) resolved); + return Collections.unmodifiableSet((Set) resolved); } else { return Set.copyOf(resolved); } @@ -802,7 +811,7 @@ public Set resolveExpressions( * the index itself - null is returned. Returns {@code null} if no filtering is required. * NOTE: The provided expressions must have been resolved already via {@link #resolveExpressionsToResources(Context, String...)}. */ - public String[] filteringAliases(ClusterState state, String index, Set resolvedExpressions) { + public String[] filteringAliases(ClusterState state, String index, Set resolvedExpressions) { return indexAliases(state, index, AliasMetadata::filteringRequired, DataStreamAlias::filteringRequired, false, resolvedExpressions); } @@ -829,26 +838,25 @@ public String[] indexAliases( Predicate requiredAlias, Predicate requiredDataStreamAlias, boolean skipIdentity, - Set resolvedExpressions + Set resolvedExpressions ) { - if (isAllIndices(resolvedExpressions)) { + if (isAllIndicesExpression(resolvedExpressions)) { return null; } - final IndexMetadata indexMetadata = state.metadata().getIndices().get(index); if (indexMetadata == null) { // Shouldn't happen throw new IndexNotFoundException(index); } - if (skipIdentity == false && resolvedExpressions.contains(index)) { + if (skipIdentity == false && resolvedExpressions.contains(new ResolvedExpression(index))) { return null; } IndexAbstraction ia = state.metadata().getIndicesLookup().get(index); DataStream dataStream = ia.getParentDataStream(); if (dataStream != null) { - if (skipIdentity == false && resolvedExpressions.contains(dataStream.getName())) { + if (skipIdentity == false && resolvedExpressions.contains(new ResolvedExpression(dataStream.getName()))) { // skip the filters when the request targets the data stream name return null; } @@ -857,11 +865,12 @@ public String[] indexAliases( if (iterateIndexAliases(dataStreamAliases.size(), resolvedExpressions.size())) { aliasesForDataStream = dataStreamAliases.values() .stream() - .filter(dataStreamAlias -> resolvedExpressions.contains(dataStreamAlias.getName())) + .filter(dataStreamAlias -> resolvedExpressions.contains(new ResolvedExpression(dataStreamAlias.getName()))) .filter(dataStreamAlias -> dataStreamAlias.getDataStreams().contains(dataStream.getName())) .toList(); } else { aliasesForDataStream = resolvedExpressions.stream() + .map(ResolvedExpression::resource) .map(dataStreamAliases::get) .filter(dataStreamAlias -> dataStreamAlias != null && dataStreamAlias.getDataStreams().contains(dataStream.getName())) .toList(); @@ -890,11 +899,12 @@ public String[] indexAliases( // faster to iterate indexAliases aliasCandidates = indexAliases.values() .stream() - .filter(aliasMetadata -> resolvedExpressions.contains(aliasMetadata.alias())) + .filter(aliasMetadata -> resolvedExpressions.contains(new ResolvedExpression(aliasMetadata.alias()))) .toArray(AliasMetadata[]::new); } else { // faster to iterate resolvedExpressions aliasCandidates = resolvedExpressions.stream() + .map(ResolvedExpression::resource) .map(indexAliases::get) .filter(Objects::nonNull) .toArray(AliasMetadata[]::new); @@ -937,12 +947,7 @@ public Map> resolveSearchRouting(ClusterState state, @Nullab getSystemIndexAccessPredicate(), getNetNewSystemIndexPredicate() ); - final Collection resolvedExpressions = resolveExpressionsToResources(context, expressions); - - // TODO: it appears that this can never be true? - if (isAllIndices(resolvedExpressions)) { - return resolveSearchRoutingAllIndices(state.metadata(), routing); - } + final Collection resolvedExpressions = resolveExpressionsToResources(context, expressions); Map> routings = null; Set paramRouting = null; @@ -952,8 +957,8 @@ public Map> resolveSearchRouting(ClusterState state, @Nullab paramRouting = Sets.newHashSet(Strings.splitStringByCommaToArray(routing)); } - for (String expression : resolvedExpressions) { - IndexAbstraction indexAbstraction = state.metadata().getIndicesLookup().get(expression); + for (ResolvedExpression resolvedExpression : resolvedExpressions) { + IndexAbstraction indexAbstraction = state.metadata().getIndicesLookup().get(resolvedExpression.resource()); if (indexAbstraction != null && indexAbstraction.getType() == Type.ALIAS) { for (int i = 0, n = indexAbstraction.getIndices().size(); i < n; i++) { Index index = indexAbstraction.getIndices().get(i); @@ -993,7 +998,7 @@ public Map> resolveSearchRouting(ClusterState state, @Nullab } } else { // Index - routings = collectRoutings(routings, paramRouting, norouting, expression); + routings = collectRoutings(routings, paramRouting, norouting, resolvedExpression.resource()); } } @@ -1039,6 +1044,30 @@ public static Map> resolveSearchRoutingAllIndices(Metadata m return null; } + /** + * Identifies whether the array containing index names given as argument refers to all indices + * The empty or null array identifies all indices + * + * @param aliasesOrIndices the array containing index names + * @return true if the provided array maps to all indices, false otherwise + */ + public static boolean isAllIndicesExpression(Collection aliasesOrIndices) { + return aliasesOrIndices == null || aliasesOrIndices.isEmpty() || isExplicitAllPatternExpression(aliasesOrIndices); + } + + /** + * Identifies whether the array containing index names given as argument explicitly refers to all indices + * The empty or null array doesn't explicitly map to all indices + * + * @param aliasesOrIndices the array containing index names + * @return true if the provided array explicitly maps to all indices, false otherwise + */ + static boolean isExplicitAllPatternExpression(Collection aliasesOrIndices) { + return aliasesOrIndices != null + && aliasesOrIndices.size() == 1 + && Metadata.ALL.equals(aliasesOrIndices.iterator().next().resource()); + } + /** * Identifies whether the array containing index names given as argument refers to all indices * The empty or null array identifies all indices @@ -1334,14 +1363,14 @@ private WildcardExpressionResolver() { * Returns all the indices, data streams, and aliases, considering the open/closed, system, and hidden context parameters. * Depending on the context, returns the names of the data streams themselves or their backing indices. */ - public static Collection resolveAll(Context context) { - List concreteIndices = resolveEmptyOrTrivialWildcard(context); + public static Collection resolveAll(Context context) { + List concreteIndices = resolveEmptyOrTrivialWildcard(context); if (context.includeDataStreams() == false && context.getOptions().ignoreAliases()) { return concreteIndices; } - Set resolved = new HashSet<>(concreteIndices.size()); + Set resolved = new HashSet<>(concreteIndices.size()); context.getState() .metadata() .getIndicesLookup() @@ -1386,10 +1415,10 @@ private static IndexMetadata.State excludeState(IndicesOptions options) { * The {@param context} provides the current time-snapshot view of cluster state, as well as conditions * on whether to consider alias, data stream, system, and hidden resources. */ - static Set matchWildcardToResources(Context context, String wildcardExpression) { + static Set matchWildcardToResources(Context context, String wildcardExpression) { assert isWildcard(wildcardExpression); final SortedMap indicesLookup = context.getState().getMetadata().getIndicesLookup(); - Set matchedResources = new HashSet<>(); + Set matchedResources = new HashSet<>(); // this applies an initial pre-filtering in the case where the expression is a common suffix wildcard, eg "test*" if (Regex.isSuffixMatchPattern(wildcardExpression)) { for (IndexAbstraction ia : filterIndicesLookupForSuffixWildcard(indicesLookup, wildcardExpression).values()) { @@ -1416,7 +1445,7 @@ private static void maybeAddToResult( Context context, String wildcardExpression, IndexAbstraction indexAbstraction, - Set matchedResources + Set matchedResources ) { if (shouldExpandToIndexAbstraction(context, wildcardExpression, indexAbstraction)) { matchedResources.addAll(expandToOpenClosed(context, indexAbstraction)); @@ -1475,20 +1504,20 @@ private static Map filterIndicesLookupForSuffixWildcar * Data streams and aliases are interpreted to refer to multiple indices, * then all index resources are filtered by their open/closed status. */ - private static Set expandToOpenClosed(Context context, IndexAbstraction indexAbstraction) { + private static Set expandToOpenClosed(Context context, IndexAbstraction indexAbstraction) { final IndexMetadata.State excludeState = excludeState(context.getOptions()); - Set resources = new HashSet<>(); + Set resources = new HashSet<>(); if (context.isPreserveAliases() && indexAbstraction.getType() == Type.ALIAS) { - resources.add(indexAbstraction.getName()); + resources.add(new ResolvedExpression(indexAbstraction.getName())); } else if (context.isPreserveDataStreams() && indexAbstraction.getType() == Type.DATA_STREAM) { - resources.add(indexAbstraction.getName()); + resources.add(new ResolvedExpression(indexAbstraction.getName())); } else { if (shouldIncludeRegularIndices(context.getOptions())) { for (int i = 0, n = indexAbstraction.getIndices().size(); i < n; i++) { Index index = indexAbstraction.getIndices().get(i); IndexMetadata indexMetadata = context.state.metadata().index(index); if (indexMetadata.getState() != excludeState) { - resources.add(index.getName()); + resources.add(new ResolvedExpression(index.getName())); } } } @@ -1498,7 +1527,7 @@ private static Set expandToOpenClosed(Context context, IndexAbstraction Index index = dataStream.getFailureIndices().getIndices().get(i); IndexMetadata indexMetadata = context.state.metadata().index(index); if (indexMetadata.getState() != excludeState) { - resources.add(index.getName()); + resources.add(new ResolvedExpression(index.getName())); } } } @@ -1506,20 +1535,27 @@ private static Set expandToOpenClosed(Context context, IndexAbstraction return resources; } - private static List resolveEmptyOrTrivialWildcard(Context context) { + private static List resolveEmptyOrTrivialWildcard(Context context) { final String[] allIndices = resolveEmptyOrTrivialWildcardToAllIndices(context.getOptions(), context.getState().metadata()); if (context.systemIndexAccessLevel == SystemIndexAccessLevel.ALL) { - return List.of(allIndices); + List result = new ArrayList<>(allIndices.length); + for (int i = 0; i < allIndices.length; i++) { + result.add(new ResolvedExpression(allIndices[i])); + } + return result; } else { return resolveEmptyOrTrivialWildcardWithAllowedSystemIndices(context, allIndices); } } - private static List resolveEmptyOrTrivialWildcardWithAllowedSystemIndices(Context context, String[] allIndices) { - List filteredIndices = new ArrayList<>(allIndices.length); + private static List resolveEmptyOrTrivialWildcardWithAllowedSystemIndices( + Context context, + String[] allIndices + ) { + List filteredIndices = new ArrayList<>(allIndices.length); for (int i = 0; i < allIndices.length; i++) { if (shouldIncludeIndexAbstraction(context, allIndices[i])) { - filteredIndices.add(allIndices[i]); + filteredIndices.add(new ResolvedExpression(allIndices[i])); } } return filteredIndices; diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 27d832241bfe..818bf2036e3b 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -37,6 +37,7 @@ import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.RecoverySource; @@ -1711,7 +1712,7 @@ interface IndexDeletionAllowedPredicate { IndexSettings indexSettings) -> canDeleteIndexContents(index); private final IndexDeletionAllowedPredicate ALWAYS_TRUE = (Index index, IndexSettings indexSettings) -> true; - public AliasFilter buildAliasFilter(ClusterState state, String index, Set resolvedExpressions) { + public AliasFilter buildAliasFilter(ClusterState state, String index, Set resolvedExpressions) { /* Being static, parseAliasFilter doesn't have access to whatever guts it needs to parse a query. Instead of passing in a bunch * of dependencies we pass in a function that can perform the parsing. */ CheckedFunction filterParser = bytes -> { diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index e17709ed7831..cec1fa8712ec 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.search.SearchType; import org.elasticsearch.action.support.TransportActions; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.CheckedSupplier; @@ -1610,7 +1611,7 @@ public boolean isForceExecution() { } } - public AliasFilter buildAliasFilter(ClusterState state, String index, Set resolvedExpressions) { + public AliasFilter buildAliasFilter(ClusterState state, String index, Set resolvedExpressions) { return indicesService.buildAliasFilter(state, index, resolvedExpressions); } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java index 423de4b43088..37957df46a79 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; @@ -229,9 +230,19 @@ public void testResolveHiddenProperlyWithDateMath() { .metadata(buildMetadata(new Object[][] {}, indices)) .build(); String[] requestedIndex = new String[] { "" }; - Set resolvedIndices = resolver.resolveExpressions(clusterState, IndicesOptions.LENIENT_EXPAND_OPEN, true, requestedIndex); + Set resolvedIndices = resolver.resolveExpressions( + clusterState, + IndicesOptions.LENIENT_EXPAND_OPEN, + true, + requestedIndex + ); assertThat(resolvedIndices.size(), is(1)); - assertThat(resolvedIndices, contains(oneOf("logs-pgsql-prod-" + todaySuffix, "logs-pgsql-prod-" + tomorrowSuffix))); + assertThat( + resolvedIndices, + contains( + oneOf(new ResolvedExpression("logs-pgsql-prod-" + todaySuffix), new ResolvedExpression("logs-pgsql-prod-" + tomorrowSuffix)) + ) + ); } public void testSystemIndexAccess() { diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java index 30895767c33c..c2000ede8113 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata.State; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -55,6 +56,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.backingIndexEqualTo; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex; @@ -1585,13 +1587,16 @@ public void testResolveExpressions() { .put(indexBuilder("test-1").state(State.OPEN).putAlias(AliasMetadata.builder("alias-1"))); ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); - assertEquals(Set.of("alias-0", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "alias-*")); - assertEquals(Set.of("test-0", "alias-0", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "test-0", "alias-*")); + assertEquals(resolvedExpressionsSet("alias-0", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "alias-*")); assertEquals( - Set.of("test-0", "test-1", "alias-0", "alias-1"), + resolvedExpressionsSet("test-0", "alias-0", "alias-1"), + indexNameExpressionResolver.resolveExpressions(state, "test-0", "alias-*") + ); + assertEquals( + resolvedExpressionsSet("test-0", "test-1", "alias-0", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "test-*", "alias-*") ); - assertEquals(Set.of("test-1", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "*-1")); + assertEquals(resolvedExpressionsSet("test-1", "alias-1"), indexNameExpressionResolver.resolveExpressions(state, "*-1")); } public void testFilteringAliases() { @@ -1600,16 +1605,25 @@ public void testFilteringAliases() { .put(indexBuilder("test-1").state(State.OPEN).putAlias(AliasMetadata.builder("alias-1"))); ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); - Set resolvedExpressions = Set.of("alias-0", "alias-1"); + Set resolvedExpressions = resolvedExpressionsSet("alias-0", "alias-1"); String[] strings = indexNameExpressionResolver.filteringAliases(state, "test-0", resolvedExpressions); assertArrayEquals(new String[] { "alias-0" }, strings); // concrete index supersedes filtering alias - resolvedExpressions = Set.of("test-0", "alias-0", "alias-1"); + resolvedExpressions = Set.of( + new ResolvedExpression("test-0"), + new ResolvedExpression("alias-0"), + new ResolvedExpression("alias-1") + ); strings = indexNameExpressionResolver.filteringAliases(state, "test-0", resolvedExpressions); assertNull(strings); - resolvedExpressions = Set.of("test-0", "test-1", "alias-0", "alias-1"); + resolvedExpressions = Set.of( + new ResolvedExpression("test-0"), + new ResolvedExpression("test-1"), + new ResolvedExpression("alias-0"), + new ResolvedExpression("alias-1") + ); strings = indexNameExpressionResolver.filteringAliases(state, "test-0", resolvedExpressions); assertNull(strings); } @@ -1623,7 +1637,7 @@ public void testIndexAliases() { .putAlias(AliasMetadata.builder("test-alias-non-filtering")) ); ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "test-*"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "test-*"); String[] strings = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, true, resolvedExpressions); Arrays.sort(strings); @@ -1658,28 +1672,28 @@ public void testIndexAliasesDataStreamAliases() { ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); { // Only resolve aliases with with that refer to dataStreamName1 - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); String index = backingIndex1.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases(state, index, x -> true, x -> true, true, resolvedExpressions); assertThat(result, arrayContainingInAnyOrder("logs_foo", "logs", "logs_bar")); } { // Only resolve aliases with with that refer to dataStreamName2 - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); String index = backingIndex2.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases(state, index, x -> true, x -> true, true, resolvedExpressions); assertThat(result, arrayContainingInAnyOrder("logs_baz", "logs_baz2")); } { // Null is returned, because skipping identity check and resolvedExpressions contains the backing index name - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); String index = backingIndex2.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases(state, index, x -> true, x -> true, false, resolvedExpressions); assertThat(result, nullValue()); } { // Null is returned, because the wildcard expands to a list of aliases containing an unfiltered alias for dataStreamName1 - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "l*"); String index = backingIndex1.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases( state, @@ -1693,7 +1707,7 @@ public void testIndexAliasesDataStreamAliases() { } { // Null is returned, because an unfiltered alias is targeting the same data stream - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "logs_bar", "logs"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, "logs_bar", "logs"); String index = backingIndex1.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases( state, @@ -1707,7 +1721,7 @@ public void testIndexAliasesDataStreamAliases() { } { // The filtered alias is returned because although we target the data stream name, skipIdentity is true - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, dataStreamName1, "logs"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, dataStreamName1, "logs"); String index = backingIndex1.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases( state, @@ -1721,7 +1735,7 @@ public void testIndexAliasesDataStreamAliases() { } { // Null is returned because we target the data stream name and skipIdentity is false - Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, dataStreamName1, "logs"); + Set resolvedExpressions = indexNameExpressionResolver.resolveExpressions(state, dataStreamName1, "logs"); String index = backingIndex1.getIndex().getName(); String[] result = indexNameExpressionResolver.indexAliases( state, @@ -1744,13 +1758,13 @@ public void testIndexAliasesSkipIdentity() { ); ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); - Set resolvedExpressions = Set.of("test-0", "test-alias"); + Set resolvedExpressions = resolvedExpressionsSet("test-0", "test-alias"); String[] aliases = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, false, resolvedExpressions); assertNull(aliases); aliases = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, true, resolvedExpressions); assertArrayEquals(new String[] { "test-alias" }, aliases); - resolvedExpressions = Collections.singleton("other-alias"); + resolvedExpressions = Collections.singleton(new ResolvedExpression("other-alias")); aliases = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, false, resolvedExpressions); assertArrayEquals(new String[] { "other-alias" }, aliases); aliases = indexNameExpressionResolver.indexAliases(state, "test-0", x -> true, x -> true, true, resolvedExpressions); @@ -1771,7 +1785,7 @@ public void testConcreteWriteIndexSuccessful() { x -> true, x -> true, true, - Set.of("test-0", "test-alias") + resolvedExpressionsSet("test-0", "test-alias") ); Arrays.sort(strings); assertArrayEquals(new String[] { "test-alias" }, strings); @@ -1853,7 +1867,7 @@ public void testConcreteWriteIndexWithWildcardExpansion() { x -> true, x -> true, true, - Set.of("test-0", "test-1", "test-alias") + resolvedExpressionsSet("test-0", "test-1", "test-alias") ); Arrays.sort(strings); assertArrayEquals(new String[] { "test-alias" }, strings); @@ -1891,7 +1905,7 @@ public void testConcreteWriteIndexWithNoWriteIndexWithSingleIndex() { x -> true, x -> true, true, - Set.of("test-0", "test-alias") + resolvedExpressionsSet("test-0", "test-alias") ); Arrays.sort(strings); assertArrayEquals(new String[] { "test-alias" }, strings); @@ -1927,7 +1941,7 @@ public void testConcreteWriteIndexWithNoWriteIndexWithMultipleIndices() { x -> true, x -> true, true, - Set.of("test-0", "test-1", "test-alias") + Set.of(new ResolvedExpression("test-0"), new ResolvedExpression("test-1"), new ResolvedExpression("test-alias")) ); Arrays.sort(strings); assertArrayEquals(new String[] { "test-alias" }, strings); @@ -1968,7 +1982,7 @@ public void testAliasResolutionNotAllowingMultipleIndices() { x -> true, x -> true, true, - Set.of("test-0", "test-1", "test-alias") + resolvedExpressionsSet("test-0", "test-1", "test-alias") ); Arrays.sort(strings); assertArrayEquals(new String[] { "test-alias" }, strings); @@ -3260,7 +3274,7 @@ public void testDateMathMixedArray() { Predicates.never(), Predicates.never() ); - Collection result = IndexNameExpressionResolver.resolveExpressionsToResources( + Collection result = IndexNameExpressionResolver.resolveExpressionsToResources( context, "name1", "<.marvel-{now/d}>", @@ -3268,7 +3282,15 @@ public void testDateMathMixedArray() { "<.logstash-{now/M{uuuu.MM}}>" ); assertThat(result.size(), equalTo(4)); - assertThat(result, contains("name1", dataMathIndex1, "name2", dateMathIndex2)); + assertThat( + result, + contains( + new ResolvedExpression("name1"), + new ResolvedExpression(dataMathIndex1), + new ResolvedExpression("name2"), + new ResolvedExpression(dateMathIndex2) + ) + ); } public void testMathExpressionSupport() { @@ -3466,4 +3488,8 @@ private static IndicesOptions.WildcardOptions doNotExpandWildcards(boolean lenie .allowEmptyExpressions(lenient) .build(); } + + private Set resolvedExpressionsSet(String... expressions) { + return Arrays.stream(expressions).map(ResolvedExpression::new).collect(Collectors.toSet()); + } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java index 6a26e7948784..fd90e1210ce8 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java @@ -13,14 +13,17 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetadata.State; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel; import org.elasticsearch.test.ESTestCase; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex; import static org.elasticsearch.common.util.set.Sets.newHashSet; @@ -48,19 +51,19 @@ public void testConvertWildcardsJustIndicesTests() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "ku*")), - equalTo(newHashSet("kuku")) + equalTo(resolvedExpressionsSet("kuku")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*")), - equalTo(newHashSet("testXXX", "testXYY", "testYYY")) + equalTo(resolvedExpressionsSet("testXXX", "testXYY", "testYYY")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")), - equalTo(newHashSet("testXXX", "testXYY")) + equalTo(resolvedExpressionsSet("testXXX", "testXYY")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "*")), - equalTo(newHashSet("testXXX", "testXYY", "testYYY", "kuku")) + equalTo(resolvedExpressionsSet("testXXX", "testXYY", "testYYY", "kuku")) ); } @@ -81,7 +84,7 @@ public void testConvertWildcardsOpenClosedIndicesTests() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")), - equalTo(newHashSet("testXXX", "testXXY", "testXYY")) + equalTo(resolvedExpressionsSet("testXXX", "testXXY", "testXYY")) ); context = new IndexNameExpressionResolver.Context( state, @@ -90,7 +93,7 @@ public void testConvertWildcardsOpenClosedIndicesTests() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")), - equalTo(newHashSet("testXYY")) + equalTo(resolvedExpressionsSet("testXYY")) ); context = new IndexNameExpressionResolver.Context( state, @@ -99,7 +102,7 @@ public void testConvertWildcardsOpenClosedIndicesTests() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "testX*")), - equalTo(newHashSet("testXXX", "testXXY")) + equalTo(resolvedExpressionsSet("testXXX", "testXXY")) ); } @@ -122,19 +125,19 @@ public void testMultipleWildcards() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*X*")), - equalTo(newHashSet("testXXX", "testXXY", "testXYY")) + equalTo(resolvedExpressionsSet("testXXX", "testXXY", "testXYY")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*X*Y")), - equalTo(newHashSet("testXXY", "testXYY")) + equalTo(resolvedExpressionsSet("testXXY", "testXYY")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "kuku*Y*")), - equalTo(newHashSet("kukuYYY")) + equalTo(resolvedExpressionsSet("kukuYYY")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "*Y*")), - equalTo(newHashSet("testXXY", "testXYY", "testYYY", "kukuYYY")) + equalTo(resolvedExpressionsSet("testXXY", "testXYY", "testYYY", "kukuYYY")) ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, "test*Y*X")).size(), @@ -160,7 +163,7 @@ public void testAll() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolveAll(context)), - equalTo(newHashSet("testXXX", "testXYY", "testYYY")) + equalTo(resolvedExpressionsSet("testXXX", "testXYY", "testYYY")) ); } @@ -202,7 +205,7 @@ public void testAllAliases() { ); assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolveAll(context)), - equalTo(newHashSet("index-visible-alias")) + equalTo(resolvedExpressionsSet("index-visible-alias")) ); } } @@ -245,7 +248,7 @@ public void testAllDataStreams() { assertThat( newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolveAll(context)), - equalTo(newHashSet(DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis))) + equalTo(resolvedExpressionsSet(DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis))) ); } @@ -389,46 +392,53 @@ public void testResolveAliases() { ); { - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( indicesAndAliasesContext, "foo_a*" ); - assertThat(indices, containsInAnyOrder("foo_index", "bar_index")); + assertThat(indices, containsInAnyOrder(new ResolvedExpression("foo_index"), new ResolvedExpression("bar_index"))); } { - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( skipAliasesLenientContext, "foo_a*" ); assertEquals(0, indices.size()); } { - Set indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Set indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( skipAliasesStrictContext, "foo_a*" ); assertThat(indices, empty()); } { - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( indicesAndAliasesContext, "foo*" ); - assertThat(indices, containsInAnyOrder("foo_foo", "foo_index", "bar_index")); + assertThat( + indices, + containsInAnyOrder( + new ResolvedExpression("foo_foo"), + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index") + ) + ); } { - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( skipAliasesLenientContext, "foo*" ); - assertThat(indices, containsInAnyOrder("foo_foo", "foo_index")); + assertThat(indices, containsInAnyOrder(new ResolvedExpression("foo_foo"), new ResolvedExpression("foo_index"))); } { - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( skipAliasesStrictContext, "foo*" ); - assertThat(indices, containsInAnyOrder("foo_foo", "foo_index")); + assertThat(indices, containsInAnyOrder(new ResolvedExpression("foo_foo"), new ResolvedExpression("foo_index"))); } } @@ -472,15 +482,22 @@ public void testResolveDataStreams() { ); // data streams are not included but expression matches the data stream - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( indicesAndAliasesContext, "foo_*" ); - assertThat(indices, containsInAnyOrder("foo_index", "foo_foo", "bar_index")); + assertThat( + indices, + containsInAnyOrder( + new ResolvedExpression("foo_index"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression("bar_index") + ) + ); // data streams are not included and expression doesn't match the data steram indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(indicesAndAliasesContext, "bar_*"); - assertThat(indices, containsInAnyOrder("bar_bar", "bar_index")); + assertThat(indices, containsInAnyOrder(new ResolvedExpression("bar_bar"), new ResolvedExpression("bar_index"))); } { @@ -506,18 +523,18 @@ public void testResolveDataStreams() { ); // data stream's corresponding backing indices are resolved - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( indicesAliasesAndDataStreamsContext, "foo_*" ); assertThat( indices, containsInAnyOrder( - "foo_index", - "bar_index", - "foo_foo", - DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis), - DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis) + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis)), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis)) ) ); @@ -529,12 +546,12 @@ public void testResolveDataStreams() { assertThat( indices, containsInAnyOrder( - "foo_index", - "bar_index", - "foo_foo", - "bar_bar", - DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis), - DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis) + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression("bar_bar"), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis)), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis)) ) ); } @@ -563,18 +580,18 @@ public void testResolveDataStreams() { ); // data stream's corresponding backing indices are resolved - Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( indicesAliasesDataStreamsAndHiddenIndices, "foo_*" ); assertThat( indices, containsInAnyOrder( - "foo_index", - "bar_index", - "foo_foo", - DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis), - DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis) + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis)), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis)) ) ); @@ -586,12 +603,12 @@ public void testResolveDataStreams() { assertThat( indices, containsInAnyOrder( - "foo_index", - "bar_index", - "foo_foo", - "bar_bar", - DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis), - DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis) + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression("bar_bar"), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 1, epochMillis)), + new ResolvedExpression(DataStream.getDefaultBackingIndexName("foo_logs", 2, epochMillis)) ) ); } @@ -623,17 +640,36 @@ public void testMatchesConcreteIndicesWildcardAndAliases() { SystemIndexAccessLevel.NONE ); - Collection matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( + Collection matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources( indicesAndAliasesContext, "*" ); - assertThat(matches, containsInAnyOrder("bar_bar", "foo_foo", "foo_index", "bar_index")); + assertThat( + matches, + containsInAnyOrder( + new ResolvedExpression("bar_bar"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index") + ) + ); matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(onlyIndicesContext, "*"); - assertThat(matches, containsInAnyOrder("bar_bar", "foo_foo", "foo_index", "bar_index")); + assertThat( + matches, + containsInAnyOrder( + new ResolvedExpression("bar_bar"), + new ResolvedExpression("foo_foo"), + new ResolvedExpression("foo_index"), + new ResolvedExpression("bar_index") + ) + ); matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(indicesAndAliasesContext, "foo*"); - assertThat(matches, containsInAnyOrder("foo_foo", "foo_index", "bar_index")); + assertThat( + matches, + containsInAnyOrder(new ResolvedExpression("foo_foo"), new ResolvedExpression("foo_index"), new ResolvedExpression("bar_index")) + ); matches = IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(onlyIndicesContext, "foo*"); - assertThat(matches, containsInAnyOrder("foo_foo", "foo_index")); + assertThat(matches, containsInAnyOrder(new ResolvedExpression("foo_foo"), new ResolvedExpression("foo_index"))); } private static IndexMetadata.Builder indexBuilder(String index, boolean hidden) { @@ -648,4 +684,8 @@ private static IndexMetadata.Builder indexBuilder(String index) { private static void assertWildcardResolvesToEmpty(IndexNameExpressionResolver.Context context, String wildcardExpression) { assertThat(IndexNameExpressionResolver.WildcardExpressionResolver.matchWildcardToResources(context, wildcardExpression), empty()); } + + private Set resolvedExpressionsSet(String... expressions) { + return Arrays.stream(expressions).map(ResolvedExpression::new).collect(Collectors.toSet()); + } } diff --git a/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java b/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java index 36f7355a541c..17975b7d18dd 100644 --- a/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.IndexGraveyard; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.ResolvedExpression; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; @@ -77,6 +78,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; @@ -677,27 +679,27 @@ public void testBuildAliasFilter() { ); ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); { - AliasFilter result = indicesService.buildAliasFilter(state, "test-0", Set.of("test-alias-0")); + AliasFilter result = indicesService.buildAliasFilter(state, "test-0", resolvedExpressions("test-alias-0")); assertThat(result.getAliases(), arrayContainingInAnyOrder("test-alias-0")); assertThat(result.getQueryBuilder(), equalTo(QueryBuilders.termQuery("foo", "bar"))); } { - AliasFilter result = indicesService.buildAliasFilter(state, "test-1", Set.of("test-alias-0")); + AliasFilter result = indicesService.buildAliasFilter(state, "test-1", resolvedExpressions("test-alias-0")); assertThat(result.getAliases(), arrayContainingInAnyOrder("test-alias-0")); assertThat(result.getQueryBuilder(), equalTo(QueryBuilders.termQuery("foo", "bar"))); } { - AliasFilter result = indicesService.buildAliasFilter(state, "test-0", Set.of("test-alias-1")); + AliasFilter result = indicesService.buildAliasFilter(state, "test-0", resolvedExpressions("test-alias-1")); assertThat(result.getAliases(), arrayContainingInAnyOrder("test-alias-1")); assertThat(result.getQueryBuilder(), equalTo(QueryBuilders.termQuery("foo", "baz"))); } { - AliasFilter result = indicesService.buildAliasFilter(state, "test-1", Set.of("test-alias-1")); + AliasFilter result = indicesService.buildAliasFilter(state, "test-1", resolvedExpressions("test-alias-1")); assertThat(result.getAliases(), arrayContainingInAnyOrder("test-alias-1")); assertThat(result.getQueryBuilder(), equalTo(QueryBuilders.termQuery("foo", "bax"))); } { - AliasFilter result = indicesService.buildAliasFilter(state, "test-0", Set.of("test-alias-0", "test-alias-1")); + AliasFilter result = indicesService.buildAliasFilter(state, "test-0", resolvedExpressions("test-alias-0", "test-alias-1")); assertThat(result.getAliases(), arrayContainingInAnyOrder("test-alias-0", "test-alias-1")); BoolQueryBuilder filter = (BoolQueryBuilder) result.getQueryBuilder(); assertThat(filter.filter(), empty()); @@ -706,7 +708,7 @@ public void testBuildAliasFilter() { assertThat(filter.should(), containsInAnyOrder(QueryBuilders.termQuery("foo", "baz"), QueryBuilders.termQuery("foo", "bar"))); } { - AliasFilter result = indicesService.buildAliasFilter(state, "test-1", Set.of("test-alias-0", "test-alias-1")); + AliasFilter result = indicesService.buildAliasFilter(state, "test-1", resolvedExpressions("test-alias-0", "test-alias-1")); assertThat(result.getAliases(), arrayContainingInAnyOrder("test-alias-0", "test-alias-1")); BoolQueryBuilder filter = (BoolQueryBuilder) result.getQueryBuilder(); assertThat(filter.filter(), empty()); @@ -718,7 +720,7 @@ public void testBuildAliasFilter() { AliasFilter result = indicesService.buildAliasFilter( state, "test-0", - Set.of("test-alias-0", "test-alias-1", "test-alias-non-filtering") + resolvedExpressions("test-alias-0", "test-alias-1", "test-alias-non-filtering") ); assertThat(result.getAliases(), emptyArray()); assertThat(result.getQueryBuilder(), nullValue()); @@ -727,7 +729,7 @@ public void testBuildAliasFilter() { AliasFilter result = indicesService.buildAliasFilter( state, "test-1", - Set.of("test-alias-0", "test-alias-1", "test-alias-non-filtering") + resolvedExpressions("test-alias-0", "test-alias-1", "test-alias-non-filtering") ); assertThat(result.getAliases(), emptyArray()); assertThat(result.getQueryBuilder(), nullValue()); @@ -754,19 +756,19 @@ public void testBuildAliasFilterDataStreamAliases() { ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); { String index = backingIndex1.getIndex().getName(); - AliasFilter result = indicesService.buildAliasFilter(state, index, Set.of("logs_foo")); + AliasFilter result = indicesService.buildAliasFilter(state, index, resolvedExpressions("logs_foo")); assertThat(result.getAliases(), arrayContainingInAnyOrder("logs_foo")); assertThat(result.getQueryBuilder(), equalTo(QueryBuilders.termQuery("foo", "bar"))); } { String index = backingIndex2.getIndex().getName(); - AliasFilter result = indicesService.buildAliasFilter(state, index, Set.of("logs_foo")); + AliasFilter result = indicesService.buildAliasFilter(state, index, resolvedExpressions("logs_foo")); assertThat(result.getAliases(), arrayContainingInAnyOrder("logs_foo")); assertThat(result.getQueryBuilder(), equalTo(QueryBuilders.termQuery("foo", "baz"))); } { String index = backingIndex1.getIndex().getName(); - AliasFilter result = indicesService.buildAliasFilter(state, index, Set.of("logs_foo", "logs")); + AliasFilter result = indicesService.buildAliasFilter(state, index, resolvedExpressions("logs_foo", "logs")); assertThat(result.getAliases(), arrayContainingInAnyOrder("logs_foo", "logs")); BoolQueryBuilder filter = (BoolQueryBuilder) result.getQueryBuilder(); assertThat(filter.filter(), empty()); @@ -776,7 +778,7 @@ public void testBuildAliasFilterDataStreamAliases() { } { String index = backingIndex2.getIndex().getName(); - AliasFilter result = indicesService.buildAliasFilter(state, index, Set.of("logs_foo", "logs")); + AliasFilter result = indicesService.buildAliasFilter(state, index, resolvedExpressions("logs_foo", "logs")); assertThat(result.getAliases(), arrayContainingInAnyOrder("logs_foo", "logs")); BoolQueryBuilder filter = (BoolQueryBuilder) result.getQueryBuilder(); assertThat(filter.filter(), empty()); @@ -787,13 +789,13 @@ public void testBuildAliasFilterDataStreamAliases() { { // querying an unfiltered and a filtered alias for the same data stream should drop the filters String index = backingIndex1.getIndex().getName(); - AliasFilter result = indicesService.buildAliasFilter(state, index, Set.of("logs_foo", "logs", "logs_bar")); + AliasFilter result = indicesService.buildAliasFilter(state, index, resolvedExpressions("logs_foo", "logs", "logs_bar")); assertThat(result, is(AliasFilter.EMPTY)); } { // similarly, querying the data stream name and a filtered alias should drop the filter String index = backingIndex1.getIndex().getName(); - AliasFilter result = indicesService.buildAliasFilter(state, index, Set.of("logs", dataStreamName1)); + AliasFilter result = indicesService.buildAliasFilter(state, index, resolvedExpressions("logs", dataStreamName1)); assertThat(result, is(AliasFilter.EMPTY)); } } @@ -846,4 +848,8 @@ public void testWithTempIndexServiceHandlesExistingIndex() throws Exception { return null; }); } + + private Set resolvedExpressions(String... expressions) { + return Arrays.stream(expressions).map(ResolvedExpression::new).collect(Collectors.toSet()); + } } From 23815590f89297c93304376dbb19478d71fe1ca9 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 10 Dec 2024 08:54:41 +0100 Subject: [PATCH 39/39] Update fallback setting (#118237) Update synthetic_source_fallback_to_stored_source setting to be an operator only setting. --- .../xpack/logsdb/SyntheticSourceLicenseService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseService.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseService.java index e629f9b3998b..f7f228859fb6 100644 --- a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseService.java +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseService.java @@ -38,7 +38,7 @@ final class SyntheticSourceLicenseService { "xpack.mapping.synthetic_source_fallback_to_stored_source", false, Setting.Property.NodeScope, - Setting.Property.Dynamic + Setting.Property.OperatorDynamic ); static final LicensedFeature.Momentary SYNTHETIC_SOURCE_FEATURE = LicensedFeature.momentary(