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 01/60] 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 02/60] 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 03/60] 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 04/60] 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 05/60] 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 zRkah%WVrkQmWuf97$^Biuu8Bd6)H07@eZ?WA)81KlEX2pPM4sbX zdInHlTZ1wMH+>gc3f_ZL*-64HH4^aP<(~VRj{KF)-_W5x!GR^r<}bTkLARQ7MJ%ia zabstrk7A5R=+k(7<_ulLTNW;IN42(3g#E1Be*2EY;P}e(YSeVL%~P!*YrIinZ8b+= z9G1bzd9)om2^{-Rp0b{NcF~m9(i#&Q&SrePVeL z9{Ga(nc5vlKAY(2!My$qly>=m>*ryh0$T$XvQ6}OsoARX6J6^iCzLX5?G!QKd(EpF zv=TmaBJjaQq8yeqrkvi#;ArE;Xj=^=9!;-$&n+@+gq2rcxC^2dKiH2wVs;7H$>ke> zEWbD=YBzZ`D2O*|`@CUXF~yj5gXBZgXT`~Kgdz=@U{ANwW^j~;nAi{Da{76m3$h$& z>cwKqWL|?9)E`wA#~A;2ML~&u=X}lx2Edi#;clD7JEj%nS~iSpVO|IIxgj)OlqPFJ z{4N{!t7CMfNTNcanGMR_Ld@kwYuFPUBFiCNMmab{g>ORXDdON+4gF8lW&^vkwT|&q z*2y-BLoopo9+-DX+*px6;Raxngw9zKn&6OFy~m%=clGmMJO9``-jJ z_7@ze$b+^aX@x@Pz(juz81af+}qKgZ>x?$d1!ko6uN{cYiNjj+9aH~ zx(T=R*ao8+Sy^(o7GY(+>wU3yLA^InFaqC3bADk?jIg}}1l4b6ObXLQpPryUk93O1 ztwiqQCs&Z!|McSi%Dp9{N-&fA<vKo(J`L;OpI0;SgzOFf#2cZ(fZ@n6DPkwdY}7B5l3(dyYOy% zR3uU_7>q)nw$+d-bvUn{mGndz9l3^$gOwJ)0cF!zM65=cypl0iTZAJ0JW@yneWtRL zG^)edkvje<#hkUX5uicQ(EQ z3@FlBt5|Vx-;G#a=wUtC6E?Yf0*_*IYS?$#=FV^Tp06^e=Q|Tc$<2dYFP^kgBhZ)Y z(=nqfhELrM7_f4Wk(U^UgKA!bU$U^&^)@GoXC>WDUc0hRg*6n$l6hJLn<|5@d~_w0 zl9NxEBU2|x3K@1})GvSi5fwgh)|%sj+F-D^(Wt(AIr09Rn0eT>Sq1u!2$3m!bR_Is z`8Z{I!pO-nWq0o=f%0q|4l*cM-LKHb{rfq(;`<=TTst`CI%d?maR$V``bb=p(TP+e zk;hvG);t5RaKe-iWz~Ff{C9$&U@Up0{VJ5`kXwPfLOr1StZX+@w0!nwqQc`Lq#OkR zDoZp>77)eYosG4(L>?S!811Us326+{ZE$iC0$K4 z7GD!ZC!b#Wl>eIm4PKB3MXnMtU$Kc z3_`0=HpmP2sPVJ@vT=9})zE(COH@$ujS+Pr*Nznm#Wx0gJhwWZrF>^l-?n)9{_Q@L z)1}Z>_5NnvR7-%prV|v3?@whe4f1%d<4e8lAv1}Q7iS^vI)Z6m#!B?9>r|1nxU)~#M;%!-ez^)oxb#he^H8qLsP$V70jom3fBfWO# zCA`QH=nN-6E4ya)G=Cg?dp)gOclDMmNh)`fGFzbZ4XCkhpDShZutrEw&iy(7V7Ovq zu>~>g_cup=Ho2Q@*k!}TDUJaLY+-rLf{}=*EHE2ibO@`OFvk zihv#ai_?0mC@fYv`{Fch>9evK@izGJ(cdp;LO=N0CHqQ-T_DVP*5zkrvHMNYYf5$Y z=tN#Ld5Q6TKQ6K}NcSrkbxL_Lt31o^Nw0EkN*6)~8H)U3fpx-Ki z_C%9M&?A-hYtIBRy{~5WnX+_*T|EM&UZQhharlXl@NZS!kH~B`HNV|B`g)vKIEqVS zIWB>Ebg_CX)20+K82(?0o38wdPm<6P9ap9Zgdct5I9AFqxBMYN{=gYc8HmZh^-cu# zACMK9<1!eMdtEiy_?dJ(3@Q?)NP=0T?*!ogeU?i}Mj4$-eaTBtvE@?zN?f1dZ70%o z%dPL(o`qUb=JN|Dk*?u%z&B3i`{BiKS@wNQ$s^v-nBRjr@CZ^Syw`6xbcWv7fs%dy zbEEaGmll`5!b;RIkm~nY1aSCU$!dR|ruzXq_OZhZ{-ncZNti)%NzUj)!2G{E6v+k( zx8g;Ij6{2Kq#&%c6~uYSt!!!_;ORpyF#Im5Z+D?C&F$k2>q*=ye<-mSbot%Ak{_=u z4jzeU_I+?-jK;j4irs>A-|pKamBz{HIw97o_K;M#&LbC(uq#ISPI$+h%`^d-Sby9L;4dNR9HKfR&Z3$H#=A5ec6Z+cnGKHFCQltD+%@SRja=Q&2Y2g3hKGOSu>B?KPBXE;Wx27rP)r*;( zGlY8~sZQ@F9s6Eu9}3}wC9v~-pkx<gV;VCZk6+(7J6;~xf3X*RNFjuea*kfjIpcqI>9HkIuvG>{YgLr# zjrXIoMtzIRY{A}LGE(fAit3|fo2uov2Ln{Nbav&-`7Zs9GhruBY*B&|{$*nwbH(_f z4ov!Y&vGuw1b2NrWd!0i8Gi6Ttdgc~w*O)v-d5$#=n#LY*(quT+hNzsR}NbgB`5I+ z)pFKa>TpPfYeZ|m`P(Pq&AhDpm?S4&L=c?nSSGbwOH|<#Po}rD{!H;gk>D+gMnCWN zfIMMt7g_D)lefxsd<5)ap7m>d;(qnOYz9G}g=xc8(f}IH;kdOFjpT$kArAvtP#M%D z>RjOI{JN-OOnyNpJI4I(kp?du`-6S7DmMHicTgYY|JAiZuQ*OzbE00nrRkL>(>W}M7J*d%}^{by;OIK4EGP6g0+}67Xv{27mG}Tv5bt+ z_9{5CCln^RY8hX?qxkH~R&fNope_1*qKP9B`kV@t2E{0^w?zKd)1}xOE zBc9H3r@?(sC~L~0G8l3D)Kujibn%H`IQI6Km)9y4_j2h+@M(}v3%o>gNSg=ifMx0) z3%B$Y&$+ff8~bc8?kCJEULr`caXR-&z!L=V%r+P&KT~E{ar*|tF0+S@?+f?M22_~| z$~FXt#}w_+=C0_n`i3w+ea%L8?Lndu=EAItt?OZc(y-eG`&@>?yvE{<tu#QukhbM<%9GCH)u}V$yHy)I?#~@RJ6VwYY9J3s?WTwn`E^otfK2P3SZj zo^R-ye>X(A<8;F{0bQm>#|Z>Cu>2m#OKey-z&&4h*?0OBURuN#9=DU7oGLVI$!fcd z_~&&JOH`dBqI74fCQzkMI;LQB<@>%P}mpQt7L7>=&v<5#X?M zyk<1`P@wYo+w5J5yhyUi<=youT@Q!3`k{|oZ?my+|2yyg&mUHD-pgjZ8Z=BFtk64F z4bGu~aScVhFk~5Z8ZseC^k!7bwNl|}ftD{e+=sE)!FWbuG?39Q{d!77R+!b-e!`H) zv|^Y~``s_9qR-Hc_VcyKQX1~(L&YH?_*&l`2J{}((O>1t7)_0MuaR!@{(R)Hon zBTjrezJ{s!xfF0Z8C+D*^Se?noBQAtks?y{@+fxX`!{y7CpstZIJ|dcB@7OwI_06i>yohMD^YokQ81_sf;|IR|+YZN09#c#Tm#4@IGS!d04VqZ)vU1$D2~ zPPyY3taG00wm58J%LP&FR_59ce(qA^*AIt{gSlyoUc#i3bv?CV-`PgThjAStxZ#nq zvEc95L?rNko1z>$XGIv?OM^#gBsJ&n#8p(CVpRe2`u{6m1fq}1cyGR)bkpJD#{vOMc=1= zW=`c6w-P5&;W%1D2QyP;DSS?0%G9Y#Hv>L*!DKE)DvCMd`~X?H3`%ictEIYAhs2R@PmCBh7oyWmbL>iO6w^nw2tf zXUJkW3K23L`3uZXy#ap8fPga@@8h`AE?n8mRTDH=C!T494ZLtYg;F1^7!7%E{r$ykzgIxj1es>bEb&D zLyKBF+l13~k-U|>_7az{QpuGQ(s|mwoYXa`l-%;&P(VsX50{A)Xl1|9)Z>!rQXIdnfxa6?=FY*^e)&`NC&Je+qq=7e$kwI#47GV&dK9||5-DU zZaZf%kcP>;-@_1hjG_fIJ%P^InKp+8u60{Vb#;~u}_jken_)5!0l&Ev z{ou7VQrE6tvsT(#42--s>9YjrQ3lRSf#Ql{B-UL)JA2OfJmLjDFV1x4D(OnZ=X2Az zJR8Es5J_hTTNBk8J`wKEo6%BIswZ<%jYu6lI35h#~yZBnOR*f{z0PQc?KNRPB*^d1Ux zf=@TTVS*|q?pz>u*m71sNrdSbFDGcDts~!NKt-C`a{m1Ez7!6+UqY3XI3{Dnzo8V; z0-LfAmqPW4^?Rpol6as@JBnNC3@h5mVlL+3JCS-%;}3A3tFY^O-#t6k#Swkk%9?EJ zde6;Nx<^{ondS>I%>I^x&U-bscCOi}o!wnhNWAxI{kV9+)OH^MC7<&}KUF9RBZFus}2L`LyKMelThP<;3-0okBpsCURwFS+bRwaWuz-ihU` z*TT0H5|p%+Z2b!ASGAcmSm;I?Xj2HD`8UhI+_d2PCVj%_6s^jeTMYrc-J!0%oTdx> z9LL&JjncPbGY=9;V)thzmyn9rOct0H5|&2`9En<|Jtto#gc)79&fb*y66?}xax z^y~t(lD*$4TcC1UV})9uU(K9&@b>=cOY!DkJ zw(OXgw7b7vTJb{Hj6cyLA-^kIR|>gbR6(tdBh2m=1^W^Eoe>eaK3_r-5rC1!Cd~;B zg;*eVSWWU47~9@?>ppAbA79@N+6SOuyS zM z%$3sxQ2|(fs>^ldhZySAA?bZV)MX_~h6cf^1O;ptl!TqyMHwku7)iW~d{_m@PM-@2 zHcu=2H@7Ks0=l!lX=w_hmelX~E5g#kLmExU?_cyUxGaf}$B+KBR`+f+YHjdr1S>fz zKlNTfUdqwW_FN9Cp=W34BeX3Q>HH7E1B9C%lDjt7Z99(Vy^prfkky$JvT|;mbmEVU zQAj+C#~NNJ>q|9FTgA`X)5yZ{1aJDrqQl+0(F6_24<~)n<#l3+Tc2IN<%0^E<;b~% zH}oZ3N#50%9Xni>V|C4CQs%|J{O6=dZ6)DtrX@?{QdkbS8sLgG3jMwkIe4{1Z_ic$ z1-^2m)!@ldSD4%+c6q_7)S1Lz`Dd2cTCg3rICcu0WnalX*?ap{|Hc?4Pf)Ns-mRi` z?pv#M0)QD0ezyslUkI>Yx{40XB@<)-DcIA`fiABV5_B&3m) zIWmd0r!HWF)MaT|!ZfwQF;ec6o|((JV@OsnWK-Or7yX4s~4ZmBeskU-<(r((?(ZMuJt~%66%LwH5w+5l1s170;CL zC0^4WjHNZ3AF1OTY!za@i?!mgo~mOx?fx-b{7c3YsnC;JMI6&WRDVSr_>%XNwM~Dh zD|r?BBJHU&TS;XN_x)TuQ_JWSQfv$9Gm;tk12gypLJB@)e9BXqjD3xXixJK(xcNT1 z86exmQ4&_NWfrTGsm=PtgPelneCBETyl)>Bf33Q#96U6Jsdhq7D!M2hTJ9913PiqNX#T<8h6AEt$QB zRGD1F$BpaJ%{rpd^J~;T*>5dgOzZ0j|`{MR_(fy+a)fF zmHZQN0W~@#Kaz5Bt}>z&RFo!l9CY~RQL-fd^oijF#V_e1u?$GDT+3sJH{wR7!tdKE$e^byAD*LBPvZM&)w zN*|xaJ5`78cw&Pz7^_|&xl<2X5`_xvRi?QNrj`46WtIzMCe`7Vu|dMWg0A@RNs^;5 z(Za!R;>l2y3nIbpd8&%Y<$7{gXZ18Y%E3@pLYlGCeB}A(fMND)$lR^3KAGM4tx$EE zdQ#%%p}Us5fho)I$6)T=hw+T+0oX6+PB4Y?t(1Vka%AJ4vs8Wli!(0CnVsEflkx<* zlzUn@HyKSe&J|yd`+1bNRYicQkczb~j3;e0$s}=Gi}qKn#;B6@%fzLQq#cGS7p8ip zga~bjYIc0Ds&g3-Dz>a-K6_Dq=q+Y*=+%3Dk&8@Ew@E!ppE7BjgDXWQZ;eXwi@3%d z#<`;(9!?Nzd`S zD2hKz$Jb-yyjZevpOs|3$@%Ps(@ybs@Lf_(f~>xzngXjx88@xs*6E>&;(@0eMla`b zUKCSLmXhONNfGznOm%)l$#!80ubt&WA^{1moZTyn8!IvmhJqS{^gGc+q$f?%Kn9IbW?4WMl`m**zRq$Sr>JM#7?GR$}lIz_c6Vn&= z%WkaJ*`T{s=7eG$Zgi+c+o@h3cx!p9^RC)6JBu(1`W7Chx6JtV?H10dmNpSj*Me^< zY7p=;O~te8KQVTZ-TR*{EIL87e!*^ zimktFxTdaB^RP6|Cu_^fnnsMy_N%>=g;fhf#~Z^6Ya^FSWpw1Jnq>#_NuIbCCUU$G ziu``x)E`6GeUa2b6D9Ndb{$jy+|KZ`4RESF0i= ze78xY^NAhnCcU??k1dnxck{V`U2Os1G}RwB?u5Q`W3q8Er|fl~HM+S??@CWTN3bV+ zi=5p}k_PMf5Jj*)$q1=vCW4q+HL#$R=WxHH5)wXk4C7H8gcrf|zId4@v^e2o-6Ef8 zJVO0Vqmy{&>Y>v11=+URy6;MK-rp6g+k&g5p(ndTbxTx{JdKW{!S9|tMT2ao0*Pyw z_Z#o)%;z@M!c+f0rp_`dsxNNah%^X856uum;|$%x&`38Dk^&+PGNb}S*U;S^N=k#$ z-Ka@BO>)>tbf9;1@mZ?ftW(W1nFaepKJ?=>-P` z>KldNVs{R5LMr*)jvjWcOBRV5Sn0ssD~BK|vuX{WOq0l3rhjNrn!9Bc=!Gm(?bpui zldge%MV|H^+%)v|e}%Lb3RWSe`wB$|FP3WRWHUS_xS6Pg8d||-)^Z<%HKpTFvv{t9 zT06bB53%5y}S*oGHL_=`7~_DP`Kcnm!OO)Ia;~b+RkLHU4ZBYp@u}#LasG z$+}B0IONOE9RJMk=Rkn;O`QxeJy=`LIHI||0DOYeh_CgaA=BV z5k*bSK&(F$DdoA3$ndyqHU( zeouEs-0<1suy$Vdd+(^W@ptvnJhBa}C=Bwk3Z3FkC+Fp;jfvQ+0XAti4ny;)4I$LW9$}{s8R4N0Jw6s;sAS`2 zTaU2$ocd4uBV=xyDR z{F{1sRa*orB$kyikn_cfZx>LjZ8Ts#5O-EP|*^{IP{fo-Kaq{M%CH^Gpi3rG}Z|6{rUQ z8M}Zo?+*My&)DGJ%hiJ9BxZjtzcply7?HdNu#Q_AU%>;4pTiNlk~)m>t&Z7nyiEoy zYqBfYsl#}A-$B-BL2yuTn=|mSURcF1dJrN*zGB0HwLwn!0OaKd4w~_y&Rwc6kloJi z4`##;)aA7#;ew`2n<^l4J-xJ$A_@sKQ#uetkyBaFR;grv##LB(w02}DHu63&bl%z5 zg+?pfAc-f*RlGsk`sJZPxEn^S2wgBoxaOWryOVh&Wp}m@j70gU3d`lYSyVld1a0S_ zgM<)ojOEPA7T;FP2q#GJ0-9?3<2n7!LrW&pn?mojn8I_^qOvl#P)C4ydv8>P(O4p=?>P*nl$SRvlugWi`_2Po`x5H#U)=I z$e|fgV=d9_>_#DS3-cn3zgMbLKWS7s0Zu??t7E6)t5N*?8W+%duF9>%Eg|()QiT46 z^Q=sLqkJRTo`)|^hhpo6C?BI#KG&C>W+1NMR?Np84NzR(o!iwv?JF}qfI35+IePhF z2hDwkVGK+8vryKZEpMILkIet-vR|5(1qM<&yJY&6JI>k6yjIEoYD0P3JouSM8Bf&) zHQrIx)V1d5@pQsMwWG9n2!tkC>jyaiJxr;26vi~HrtZXPgR2ZWc$1}W`BGq{TGjqp zGT+)uAS>SSkFVO|WiPWU*vJCIgio8iC3z&jhOJ7K;~6=Nng?HJ?KVQJYDBgb8oEU~ zGe!{GZa;7&$)b*w!-L%FZ6ih18Cm^1r@id(r%LyjiXn}6@hU!k4Zo|E zOyeZiM_|fU2aS%-QdheDgHWmpWh3-_#Ma-FrJhCGZ3?XrcDJieh!Y#-PA7Gr9wdE3 z%JpojVKhZ3Qov4u_VTXZ*=gk*%2N@|CVoZ((T5(UL}|UuFns(XwX%k`W~3@|E6}Qo z)K%rf@#fH1+d5O9{ALxr1aDQ4&x$JeH=*jg-$57h&?Qx4ok3`eZEs2@)hxT3#BydtSgk!dlcr3s3DFot8N~VebBEGI z(R>N8|D-eW1IXuaA(#IFMX|K-3_4eZqy_&zXB2YoQD z>LoS}E0cWp8=f|Ey%3$qy|^rJ#y{BXAW1P){Bi7biB`Fm-qESPF^c2dLN%$hSc~)% zWg!r!q`+*DD1<;%Gk&UW7v-vdNsQ8`AwAFZK1}+|DI;NGqL(b~w*Na32fA|4DAwqd zvg=OkG0w2Sqc&lwxooeKASKgc81epg-T`GyN?HTG&H(L7+LkICp7NQKJo?o?u};&A zg9Vcyd@r{^5G4n{jG!hU7gNnf;;GIu=P3j0moIjE%L2*5L*ChdIzN$tuBj&peUutQ zaZ%tL`{k>q5+p+-dHfPs$#rt^+@2dovY&4vg&E^FN;&X{wpu7>RFUeoA)rFX7<^&c zCzm9+N$Pc`nd%Nw-{IA?7900TvnEZLpUBJSjEM%J8P25utpe@xp4duYsjT zL=)%9cmE7ofqE2r__nSa@p>ey;Lk+WhT>*%(D2KnlDyCCtvv}vXS~(wHa{M%HMAn$ z>ztE9|5&xtvDNM_Z3J^hrN912Fv2~jU0L8Orz7{g2oZGHzR$1KI}4-NA*e)==QlNL z^tmfk79a1h;aRg2K)WV&GX4phC>kbHBD*zyFf{1a@~c}gk#;vm|GA&NCOcL~IxTE~ zrrv7MMS*cEtfM64u6sov=#VJ#j(*UVEA#u*S0^7Yk=I<8CaAK5-mW79yoOXkY0`VnC^P7)QdJtMud43`mDBf`EFIaFa~FZMxf zQb8LleyN9~9Re)^~=@D(pscaz8L#fD8}*sEO1wZB)( zytf;XpRgaJyj40C<07#I)Ej+^-uK6dU#=pEdI+R(2@`Ebdb8aEvUk2W4toKGI;O(D z%@vapwqxCt6!GR{?OH^Ton*yUkTG@REI7dqf-W5a`tkYwVD z>~owJfhj|s!!b*0oBl&!DO=G*pMPj7F$j7`@T>O_>U`|ddO?FrqVnJ8o%*3k69Y?L z)%Agx^U9sZ8_aKFQ(W>ba}I}xB=*&MTh+l`m_vL{ztKCZ#0jrZl;Y`*QNFt6Ryc&wGNyY8V$G212ute(^GJEcD;~%}n z@VuY&DL&0uIGR#m&Ql5N$8-OX)!)-^SWk7HJ94JPkrE{{M7sGKfLwp)ph_W4o|~2= zc*soY?rL_<%kyyK1kKl$1oJ(^S86CLHb5j1k1}(h$lf!S@t5EaJ`8+3gtkqj>dtgB zv76uZQ+{@t*L$-o?v0Mh`Q^Q)o~DEv>CJijU5z`1#Sw>A%FXeaSa`MlM!HS}q9{*h z==?z;%=v_BsPJRVm&O`_)OQ5Q0cb{py`5{ZDv$UPR|7KQpN`7a=o$gV?-d!ICBI=0 zhRKHC(B}QsqxAZY(pb*4m;R0KRMR{wrp?LXZl1>XbdQ1=Wp(rpODnKjCx6(}AgZaWn!tXt}7b+ZFqc)xonH^-kfkw3zeK@3hcx zn!fLFGEBB=5uJ_MkBE}{lx=X2B8FDqBGoj13D@hGEg7McAAIpW>$KhZ?!OCE9Z;CA zSn^CZIzMZ`b4N(H!|hew&yL))bVZ_xSmC8ioEZN)(gNqAcq{M_x~8AatL|O2pONcX z+iR3^LbQeQb+&fx*!59o?YY>-yONhZ^71OL@z&CP;=g7YgUcgqE^D@rx5)YH6!Hrt z>`t3@5`3vbK)2di(~o)D)Ytj~L^O^ZYORcgs@g42-xtb=&%45?zj*C5?&!j1r=q-U7T&PuU&G_Vl8Ryt|M}g!_+wsQ81`p3(L(urPPlS4e9t9LxPl zn855=r69i(Qvaeem;E>1@V%8N-jc%VkZYuNKz_jU9cZJ%l8*+52xv4ntcCZ4xPBHk zFK8}h{=X~Fn%ai;ONVB2c1mX2Bf>LDh=HZ@?W|Y%x99S75O1=|qWimJ>sK+&@&`LD zn@!%=q6^cmmem4qtWjpzLz43aevh;qM#Z;*8TPr}h8*LU%M=aikNt5PTZFGa0rcA> z7XXuSd@V61yEZ~V{;{@6eb!^s(MvBDt_h7(h0$P0ep}_ZoWQ@Pj)BaMeBy4Caak7Q zSS7pZ?i(36*RmamA>T;-H?aEomv1gq;KEY?{#pR+0vXRZno4c5U|=Z4zGU z++5za+MWEcJ=sO=toObB3>3e0sJl~{tP9lsCglV5j$6i^r@ZwF<`e*mra{ZKbFCFr zcM~*Eo6%+VZI054$!ewL8P1RR+bmKDmKf|6Tjx_KLz}Z!n9Jia@e53c1wwvEF=1Ie7#oSrE?N~Gy2l<=)hcqdz z_ZX5*UYGaEzbKd0L>gh#p*6L7&s;0==jXIPnKnPJUV0n^lmRtW{Nio0I-*Q_&`dwc?ru~h3%}=#h4#4;3&4Fb0k6cdUz4ssX zBhY;T*KD@Kue+=*Hz`YFnGzz&prQ#4m@`qc5PW&0$#LFIyjDfRsRlx`YTt;;^c8&V z`8d$hJ7)d61R7Rczwc)jv3bq&+5XJ5|EmmPzqGksnKF?0NsST@v=s zS!?a5LHqcBh5+EBxCv=(`qf{tnoAd-;3)UiD9EZXty8lk1J%EE*0*^YR3Kk-nm^X3 z(TFc@2r~EBN7NGlgHcn^jUn9YQ?W~hEX(6>K?sdhz6Th=G?OZ($ILa(ssV1!| zIp)(}V68+V(Uo*O!`1{X3G*7Z0Lc4~r>$2D;>LT6fr0o+#H*?v@3MEQu`NE{d{{wd z7;ENw4IbG3#NMpmpGy+L4)ZuorrC`J(se+W|5Q)+T${>(h$r&d{q^Fitg8wU_<(of~ z`-5sGAv89IquD2vu2Pzv*&J77C;dLPf6M~&4g>BJ*47JOpIR7kl(RSYw*9MpQXd6`nMOnY#&r+1AMhj)36}D*9|HS z{A=zC_dXv}yHtZ5-n~=hSHuBxi1icaik}=o{5KkYK)KhSgYXS`hd#|R(nn`}=3DB! z^R0J8uOT8iKGiGzcO`2N4g}V;IZAk6FYwg`vN(zubdw&eg=Os2sR|HtE&WWZY;bKmzeki1#nX z5;FJW^v=g@(`?i~P$N~XVl~-Bquk`@Kz7>`^?)gi&6eF3XyW)Zv=8JD{X+;4oFg8J z*b)(46v803(={yD{v3cOa_0L8mS62>Rum}q7hVZIj(g#BrU2Zgfu8W87Ic}rL5)Sn zx?mYJ#dnwXN-~|O7xD_9&X8$DDw?CBA3dgk>JfMt$7=;+`E9|FOwQJ?Vt9D17qAi*9>gB>30eY70XGR*2a&a>s03p^GaPwI5u`i0MQ>Fkn~SIT_wp>7<$WRt z&KLCSQ8nNip?x)x!~}oOb|86*ER=Lll`<(zJQ@Sq6Kg+e_`s{^&?r?Kcg)6M+cA;p4eBh=cHktwUU)3?P($S zCOnuGgc6fijI@66O~WvFXR4WDS6Lfe!nMEnclJRPIEfU;Y;jL2@$-v2$*(ZZf(8mB z+HlreUIGgWXYS6%SY7?i`ptEgx3icmoi#!yHu@Beg*pW~laL?s;rn*MI}`cw2Y%x3 z(nRD702U9>NW3+sWDm6=a-upd;E$USuBcv!XZ%p$vQ znc_rzR6ka4hirDx+i~}k19TOcTVt%*u`>5HTzD;)pTdiJL!!}%kA%%8YX?8vlI~LkHE8H>O?UV1KIg~AbYEm#1Le$@j z^{tFck103ZXILIRXKfYV(<{yXWZ-<}TaX^n2pu7um1;Kfck)2pu^Bc4ag6s&6ps~W zK%O6$Mc8csM;=r0A54zfROtL8FPP;+lc#ZA=$yaQvm%F7ha&hUcq<}j5}G#8mL0#| zS4TN4-gGi%ck6{=x1(F;sD@vH_c%SpfBMYp?5XKMK5h(fRi5v#3@jx+&q$}EFm|JN3eL}PWNRzYg66KFb<{L#m zMdw)~$fgSPg0Yg=d4-@%AXfx%k7*12B@y7`csM6ku^Hew5BKV=N-iN@1#*({$cxQu z6Tn6$1xG7B1YwfyiFwzm1#6>ju^Y?5q~ zmHjkZ`===h94O0uC|)C;nh@JU9Q-c%s=ShoU)P^VFV9fQg#MdPQU2w$BK`7t64&Wgk>sZoXQeg zP8082#NAH#sSw~Rg1t$|5np~3%_}+fVi<&dhWn3xdeaGN^O~OOax;i*T{v3}V-_3Kn0n>fI={! zBMwisY3)>$doL{Rb0}JgIW;`6EqpU_Lu)yLBfY`U5BlJjZ3St8v`uH&_Vf#sA1<8M zL->}2P3(MP6?;Lc_uTLrdMbGdZ;H`+z~Hk9t!b3 z>-ysOYbQD5eC}U3>aF2iE)^r01kWN}obX)U2jEO-f|bD}uWqe5)>=&p$G7273LJdd zHEl27{OKWa4EQ($lms<~5r|WnGj0;~p1kwcHEvb;M_pFEau~tsk+_w5Q^i;`BT2)m zO+B4lf{#_Xz{ApRi6?rR80$`p@5epJKA!FU>uUwE9B<#XW#TdU6aq79M0VaCPw2a~ zu7>0vo;{H@M4GISRXj=a_@5bbf%6D`#bq1Y00pMf5dH_TpEBzCFO^$RGSS#0Xip(2 ziy-Sm1zN2}Wv_{y9QmG~JuIq&&~>>*NH**+1WdlipV?6v!TqcwTB)h$9dgy|8g1CAbPWP2PI)lqorN5SG{Jx#w*LC=ccic}zGbUZr`%zJhjNKsCC5biEM}r3n(NheWd8~DWLn88B2h2b?XmlV17AA5*|-DYOVaN zW5z|-5IJa{r;$1P)9YEZ^$;fUBHLAR6Lwb9Zj-@IlYaW=U6B@@T)xHocgAn8w(YKj zX53k3&kdw~LFe|bJP7EX`voSw&W*wdx5TNd=g1hxm2e9U{?Kj^aa@bD(PT7Nn2ZZu zB$cZcrREEIkZ(?<-^5+;+s7nHN}D5y5h#1nv9ZqTc>Dw5|p zAC>9pBa(336P2N3=sGBnyv$~-Vbmp2qX&4IhW|#<{ryDQ#XK}HXaa%?Av}L zGp_qQHP} z7G@T*=jsBzma$f;e&ybo7m&$1HK~cZXC-;1sa&BxCk0#5d(@1T%;)uf@5p<7FdwAi zhmJ@m&lvH(t<>pGp>WI*`bRQXdXFk&+AW z*`!#gi$qZu#^HDM6>^49M2Ye&zKASY&bVIC+qlSZevHnM|7KjS7PUYOn z);QugRgMJ`OL?dIDjo7-q?BbG_jhf;4+UhbdHY63lyIVykcBnI@9{l|c{VDP!F`*r zKagrF!tHCY3W-k?IZx%A4nZsM`RccPY=*IloLfD78l&+(a~njp1}0OH=>qnhqhh6; z5E=FSyyA81Uz+(?Bk6o%v#4(B$3Y2C|DAJH`YdWWE>!)gJ{Iw}y~DVa;ddSMJgsiZ zJ^%VHTG0Dom+%M+BQYUwmX~DR8e| z{H%(#%52;;$&jISH0loAV*i&q@cy$*mPo2QAr94|x4FKa0NuzYlFe!cQC$DxPQ!i2svPnF# zzCSZF+L zXC@sy&z>mT?zyAAUsY_!tFH2QCFWyEMGGb!yPcCCbI;xbIDRwx(I$C=^CkDi_Ot9e z(9+L{Fy;u&p8p%4_;XNXeU+F^pvrs%nu3yWF_sjuUXF(fOJC-e*f(U6~Ol?zbF{d{hx| zj=|yF!}^iN0d_MPcw2;_h>`3b81C`uwv79S_Kv05Pstal7wH2kkx3F1DUWL0dfyIj z(SE=$MnfVUU_( zCK+^fEw5cY@NLs4e>j*k6MfY5Fy(PHK4pqrlLqJR3D&@)gYcla6k((ut`{ZMpjzF4 zG5vdiO%oOECTW$7^63u&tJh=8eHjTe!oNO__KT`dH7t@Ad$Y2Vo^c9Y$t!KqMYaSh z6e>$Ah`+a80ckBJjR<@x_!zb`UIkrDJS$mQP+6=q47(at(^?c^A3(hP^iC|?1J#Uc zXCt}zkU}!n-K&$lET7K|jx3|Ogshk`^GHIfEJ65T-HA7KNspZ+hl_mqugo$;CU?^v z{@8@T&tc(~M}?ZDs)9(JH^Y*nrdU$O6S(!U@BVqQpR(&FJgXG6gK;p#YV9)3olFFx zn&jgLK8TcH4C=qKn^9Td&J?J9HoBF0%+IEGg?(j3ney*6un8aRcqBCXa+{mY>@xFb zs@9sEG{H7U4I&g|2>oc#q+CK$ByCeEW%c_l9CAvZPTf5tJ$Qt60GID)Xyyl}Or7_) zGDCmE^O+d}8dKO?!w)(K6TNNo+CpQUm{}pQ!puEB#eDH|3oret{X{;u?o3tKqkln+ zMFmcq7nS;tXQ=P=e|Bj_l7_)o0b|>4oGm(B$8GStaIT;2^)D7@;-y1NEWJ(0n&hT< ztk_GT62^Lm3KPdO$yUG_9I)2AklQVo<0^8k2{ih#F^PD}Z6FqdrA;OV+Z{%WXqG@B z#;y!ymWjPM>sNn*iuFQjmKbBWg@;jO?i6c9&D7w~PKppe#}_At3i7f^ovaLnTag0^!p8ciDAQjb9ty^I)&XagS<7_S(l&W`LJK|-sEdWD zLFYH5=GP}eB|);^L!)X*ZvK3LoC`lwrR#B+yLF`156Cai4Y{5;OBL(cgmbKT@EqjM z))zz8eUfn*uOHe=O)6EfuF7sW8R34+PsCi9B)Y5{7dQbAfqeg>3_gc=%ev`47M$RA zl0Pp6V;nIDyBAURjLjLtTZ?4X$wx!tK_|U?MHps^!)lz*`vUenIdSDTD1H6RiCkY= zGSjQkvUBWu4n?PPopn+se+A^p(w5_+WU;JZL{&=Df18UZh1)fqH*nH{op2xOJ@GeS zSs1O=+sLkmTULy&a^R2DMGUA%d*N!oq$BogvOaMtGLV?JTU#P@hNzHe16WEyfc+0b zl+4-NUY|szARsm|JDTEkq8WQ@vL>qZDwZ2`rX3<*!nK%o)Gbt)CkW}cYf5@O+q)}# zG&HBf>1!7akGvT|g&p}ymv@7xZ3~RSTTD7iCr{^kSi!RFv5leYL zrL{WVVxsR1t|85dw%K&3*HO(VH^<)V^VH*k=V5HYJ#@wFTMw^9t}NLRy4y#(;cy8G z6?s5MOZYfdrd(_5dYC+N@0f}m-3s&g6DUfCXSO|dUW~Q*-y0!Erdbh1k;2iWh<5Mo z4nKOgu9%4kDSuOPU|9P!7EY56p?=C#S6|cpN@>*c{juO=&!(whh#Yl8xXW(!)LVQT z#bt3%TOySXI)BLgkzKsqpBf7#Rl!Z)=A&Q~^3wy{`Lm7`QPWsMs|)!gN*w1nx$EYXDP@++S1sYyqn&<#F!vR_sMm~U1M8Q`(mt;Z$F7`MKg;g z@Mc>M%Kn>KZ!3r}y*DUAO?&qQR~%x_R{VHpSHnED;y9#cUB4G5*W{Xq2Xk zU^a+aM1kcYvDpj1u^u9`VBu3sX(D5RL5dT!kd0mkRD2(AZLhe4zXbyq~xVFGm1ADr@YT_yk7UW;~i&V|2*->yx1n}X6hOu z{{hkd_2OVb0u#i+>4O#0u^9F&VI*%D@o6uRN+9%2*OK$^byhRp2wwGG!|=SUyW#)) z$!BS30T`)YW z5UlSi#95{DqP~Ibkdm{FlxV5uVhAIT9ZdsMC^!nMDIOfFIr9Cg^EWAe!T&)u>o6U` zj&X)cVV!atp?fqjwv~aH-9(9!QB=4sW~|V%D;uXc%(&th!-hDyLc5Y-=s_N`r!d)u zC9K+!;`guINp5|1Q2$ieM+joT!mc$zSL6|HAXS*8Zvwt;p%G)JBCvI~vg`LL)AUY% zy2|WgY0MUzh8+$aZBtX_Vcj_N>Y8)L$iQ?W)BXFWSG&<5gXc*1-r5TiU(#c~w8VTq> z^E;)*VmC|}vdB9P%4A)&bRdWI%A?jLONlOCz$E5=*4G9u8ou_mQ_ISj-s~cFb>}o` zl5tO5bktrqnc*mjYYE=yg|#az5nRrP6uh^-&C-|D$A|`i;1V zQhhORN@URF+hkQ=0nct`cmDWSEFg2OGxRl$O%qTdnkJcb6=h+w`bE-J0MjgRk;fTh z{jwhS_AbR8!zN4{vB`IaFK?Z;k$?E~Q3Cpk`FsX9NIkY>-g&jJMVX;MBPL9S{19TG zx-+7J{qv;Xw!i&UO<%x00hupkPkxG6g2aH~I)@+3v|+ZuuK zGNc|UY1pkB4EQ%Tj(-DPEo%)JCjmhK5D1P(tbPAg;Qdhb&1T2tMb59PrB z);~kzh$dPYtgj!ltGBkkJkqSBmV9qP1MrEjo)6T<6>dHkCYyt z6p2we9x}Hbt(5S!j`W2dGuS$)@Y_6l#mc-lR|fxG#G8sH?@kCKlqicBR#~H)8p6~1 zH*%{z+ANM-*PluU?mFtVFXEg2u&l{}wTk(sX%35G3ObJ|s#a4LWZwD`+PG2H_2NZ; ztvvEu{Zg;^XXQYI?G9?XTTJ7BbX)u|*n#13q>fY!P5FhHuzXNKczuQ4x#s`&;oI+>8N^iyt|2{mrI;(k64QH^o$ciso}7N)cT+;VG?@@ z)O$fy>j^!O2i^nMbq8k|9VHKi0OZ@g^;L&(Ow!HTB+5O^VUpPRPI7h| zRu9q=E~3SfN92l@7RE=?K~_DX{{z(V(JMI z;H?iYV1wBU_eqd_VT;73PMyK}4eB3Wf+eD(-c{&nB@o z?b1b4b=L#4#vf$=W@pV#X}$67)CGS>(WO?F1;36mSPPH`Th8MZpD%eURQ44-lU}gQ z=yg&pK6t@N9ho-K9HTgwV8lsOSN6++L2i%_#70XK1v8Yh)a!AGJ0>V)wO!7jh!kZ} zsZ$?ri1ax1X+L8ctd(+o3Z8*@GJ|qpd#vc3od`IyB@t1J0@0&3)If)P>lww@Xgt&* z=y7ClNrFt;p|pPQ%w0I+;|s=(JDSHTpikfU-JlO~R8rd0?0HtJVogNllO%udB; z`!C-9HN|!{0nw+%;^E^aMbZX9xOE?+1(3Vfr(i!5%su;EwzB4UoVKg#&N{)y0NBjZy!3 zu_vKWivBg^Iy`1M8W?NSj)d0!<4kg$U6>F-U24q8Nc6XuZ=)k`7Uw4gd2m|o$ zPh#1MvRWgr6I`Q+u?|zm?U*J0*OU;e@b=hIs{GN=HW124d!7S7QSGq4N4tJnXZ}F@sQQY(H zjhPXqR)GJUx8=#r0!O^GE~$(ntAj3mo^Q=#x0bCOp^_5TdGW=z)Kw!=+eD7=IW_y6 zB#v&H%xfi0>)DMo2^vS2hZnJ~=%gm&N=KD7heH4+2k1v1kM)X@|NU@?m z0LkdR--IKT(EeaH^+o)Hh`+&z1`WcE+Ng-GEO0|oUDm44YLH}G+3}7}vvW1RNfmUf zlr0uHJshCSBf6t^CXE=clm-T}tc>LvY_aJ4eE#7neEg%D@4dLPP++r}z{_mJXHPqefC- z_VdfMF*Rf5Fswvc25+56nGye<#KZFFn>6QyejA-6ei&Jj;!DP=cFxhet-ut*jC;RB z=8Pc8vA+A0s~3tO(0`K;GYedcK-^%9%Ko`j7NWRSy?2SRz}Uds!rgx{gDp<#=7CgZw< z`?=@Mbc{ZgXSVj42u1Q@T5KXnNI6FPj-W`5iC7G8(ktR2BjYnO`f zBsKx*_)G=;2hW5>;uY}7apUt6px`m zHe3;>&|IRn=(^~ByehasO0>0+`i_73o>LiT$BH?IJuJ1z5y|nXiTN(;6>chuwG;XH z^eg=5-kg`UC{g)0@5Z0R4nTSoq;gA?l*M3u+Drt|k{SufzqD4cM|~dg ziROLsDDJH7qN&7iki5pzRnnv?vVyUVfou8T_2R?uf>bEHr<(U?Co5aCFw5eABUu7{ zUPjq2al!^yC0~CD_X&}^lseD$hHqtAc)&5Uo{d?RRSFoi{qfrx$Xw_>FM@2r2^@b9 zgh}ZB?=fS3@37b~yT(g3f$DHxY}yL!6s>*4E;AbAsyBxhY_s^T1^Q8VqA3@xB}Bi) zIv5a%%&(~)YrwAOm`-tg9?*~dZ^fO@)W^cwsmQ*`^ITRNo-K+>~ z-$cUdW#7&>?bIhu7FLPi4pem~cS}D-VCp0u%1S+DKP=Q3Cp*F)aRUI6NU{`oL78{o zBT@CM0=7@r92@n(`=@IleLskLM3mI4`VrTh@>jdgxF^hxJei$wwe=N`g4IBvgbv|v z{B6)x3~6yAskeUCNl3Zsk>;Az&OVm^=${4oq?Vm!oBZ*yKlE+4QCk)@gfp=J!R}k4 zL6&#J0gPQKKr|Q!D0HUExen(I^u2AJWNI)oeO-;f=wn!oH4YA|$FSk3t{#e|+3bSA zTd9ryK6;dHP+HD#+zEv2SJ4BuVVV*m;O5O-s&W8xO@xctl-j^I;aL+;)fH)OrX0NPbx87*5)4z4>D{b<)FYTEQ>(nn zKc zc>*%>lYFt7Nv9B_G{SApM@~NmdrBiV|0up(m&$VkKZ+i7_>7Y1FWTPNTkA@Q#Lt$U za`k0Z67Vgym($2+v*}h)hI9dmKe`a-pEI*&_C$=%M$!f$fLSjcms%Ux7j1{xV z{RlQz0Iw!?FW2qEGjr(>%1h#2iHPq{fCq_sxA2KJ{!&o`)*0}6VOV)7KYBV7&7^$A z**UrqRkx?e(Zn!;Uh6wuXoF1iVI8$UW#TZ^zG8#5-0LT9ZeFCDzpiu!kn5{YZChsT z5C1cR6fEJ%-hFYeWOS5-OzgW#nf%Xp7dTdT0)izAdpzG2;P@K~6b45NL~W-SJ%%A4 zY6B0PH#j+ z*jtbj!qpf&UrAoS9o2vwSg@Yfzxf7*QLX4w$S0|pj7M<@3AOA&RObqnyAzVNWPGt^ z;sfMrYOINj74zV&TwmxR0sXco_ZV$_PXH0?NJYq0seCm#75Li|pDXHsP`($}3Na;Y zu+{R)pCXBSeo5PkISUD6W7BzZ{5FMso@+11ny@fT;SW;mH6?c0 zp5I+bIXkP&b1in;mbQG`pC{BH_touH|8`36J+GtMInjLlu9-9?y$ld%7%?3?&)f}p zTl9GO50z_0iG=i+z4 z^gxGKaUlM_cMyxW3^3-~Vp71#^%?(zp%1o_1#(|~+M~H6#Q?oM^p9*Oc4!`H5>62( zAGtHa5&*VpQ4D}V>i+?BD+nIMz6Q8!;ZO_gEC?H^B#J`#qP|okudbc3H!^%X(ucv^ zdAt!Y&p56GN_kmsg*&{pmo@wxHnrd8LLFf**EL8Q>YsIF@rXJ(Z?<>aJQN*_iyKGc zDu))ZcI7v5U-y;G4{Iu5RO&qfVK0QR_tOdBWl-He2}g^?yJ5Ce1cPs86Ci@nDu>nv zq@$EyCqT7KK3yMROF`!x0gi8}HUnTQL(o$9GEDy#ps4uGMz~m|v#DveMsOr#V0etf zhfqBPxkicGN7tS zm-Cdv>guQ>B1MBF?~BmtG@+J-pQ$se18{$WcZ=sNrC`>{AF;;d+z%*aet*Q)pjIZ@ ztmnwCTB2Xn4crDCEylPHN;tcemtujP%7r1~gc2nhsR91?O@Qmvxb$!{(jIy{iSVE} zXO@d78E8j|9v6QOAh#1;L^lLBA$kmRAk6^}4G3)cSlJAsDh57lVm08YUTHQ5*%Dd} zAD-5ti|z@od(M*oNr7M#cu%WwYOYQ}z*nQI_exM%#+@?-F}0FT|YA=Z~? zPJAr@I#3Kw-+U$=3Kri?H!w6A7%{|p)X2dYo`ZBg`kv)H255Y&IV_$hwccyFIiiF9 znMx_NYKX4PN0ExR<-ZmHlr@q%SCYr8-(BATgYy5pg2@NPIq06-50?ODf5rEF#U;=8 zCk^Sqy>!bqoA1r-&7Ucsdx>!*?44OF!0FEgG4C;&+^g4jW{Pvst}C=oHQ&>Wt?@@uzDFQI9@d;1?~_F#P;vMRNw~J;x4)xN9#i<=IPe<*OvBabl<-$G&DbwtCpSPe znZnQIK4pq%KX(L@1-X)>U6U?PvoTZ@WelFPI13i?|!@qCJFd-3oy$5?2@u} zFWq}CfcMnyA$}CQBgv?>CJKmWtLsANO^$Ik*5-EB0Mt|tXoueMwwJnl9Z<-1kS6@+ zI{Q@{Fc1YFJJw*fHuFIY(vUWPBeeT7PGPJfJhG4COpzG61Pa+(4sS?dWU~q<>WSpX z6+ck28S7|D<(#~R%OBEqORV2QX705ws!4W|LihFpj*m<3ET%3JkT7-~y%$Z}gnUhC za#s;eJT}b~n~}h7`$hF*!;RXR)kx9ItstZT5Xvr33w^b_Rq?fu!a04kd7wltQc@G| zjg0syA&_Nt`2JdVj+G6p{5Vcpz09AGi5j=WZsaXFE{VIvm`UmSrAG-z`edFg5Yo9K7dLpX+Ym@9}mxD8GmH9#R<6A@p<8~ zE1$VNtql8H{+NuUsM4luT;-S26h!tpj|J-rP%pnhh6F9#(^O_jW$huRrIIG`8}&kdvC>jw?X~UxEx8yD(f3p zNN>Y=MlO%d_CTT8qPmYcfL}^ExCP)lA=&ej-u4q}7fRVMTZJrh8-o4pm`>SD6pwjA zjZQqogF(1(P3)W}r-dZqct+rnL_i1BXQgh?*=C62nV0^qKL@$7KL1!2Bzkt&IrA|C zhFMQs6pEB`2LQ+)`z$pxEyw>wqmxq5Lhc*=v*Ak(zJtsW9yIhlPZs(dU4yD%1_-05-!zKHBfdf)o@#DNywp}u~t_ua4B$(ZAHN7D9_6I{#<%A3tfj06s+G<+_pZLzv2I?$zbhVN2niOGngO(B|T z8YH(wC8piJDk5s(kN(i-Q#-OUugkfi)RUorQ>g$@jg3hqDjmk8KjARNz7)}_m3zgW zgCsgRP*Tz5S^C)zAjmG;L8NN|`+R|2qMvxgRXgdZ?uZ?XAW~fbLtS;*4`Hgx^al8M zU*1`$4uZgr<)HsZ)>}qZ-ELpQ#HK?)kcLf3DXDY|64Ko%-O|#HAdPf)x0JLfND4?d z2uP=NJlEFyKIi=3cZ@yw;s$@Qt`&35wPs(2NpLl~D7#$P3-x`^jG^r@)+*}TI(H!MRPGgy|Oe;DAisH%>yTtp_eJx5n{pvivXoxCRiXu_pn9RKEv@1->H09 z#ieGc?x1oUcjo%WV_B#{dsJ&wSLCooB7Ks6&Cnj&uY>Q0j&$@;7q!}>6zFWe9eFSD63&Pq|+B@AC37|SJxqHrCBGL%~C>Y&F zem-E0IOea@Xb-+19B_h7?Pb@M?zV45 z1d=W8rO18~1a1M(y)Ot4)9s?kWr5YQqAm1oOz&O5@Au6UM7x{ER?N5Nkar_0emSYo0ut^^DHe|lnlu75ks9i>dFz=k8H>4^3X^6j`sRl?_8hlOWQ zBSRluj3zW}xkBct24vz~!(HLn7<3wR-pRiDCBl{Rfgvg_?<~p;dXHxfYA@>ndde0x6-aBI?CRVGGTuW?iz3N& zhTMo;fTc2BS@gZbxh@HHpAQgFEfA?pv^9u~QKc(>q%nu%7jqg&dDA(HvPxH^l_MOI zAf6@bD=qN&Hq_6?CCxo9pG?co7={@RQTa`+KD(80QqU>(c?rC%4~yU{Pc6uI?^^eV zBVDF#AxUx(f=f%V?Rp&pRe2>@P?@ghU+4-vq_!D$&uWE4pw941yG(0BeTj6iHFIxK z;nj1a$oVHTK)XaRq4Z7)J;c}cZQK*Ae-o(vwZxa>;jgEb9i`z(1phlhgZFg@aRJ6*~pPo6Qx;! z$*(VnpSb}kew2Y6rYH0Xc-+~l$L0u?Iz_QI4Xa&qVKT(6VTh)Lb6Y`~I>j2b2A@8c zAyEh%=c8`?BoBmW8a`2=L3`a6Z@YfzgO7(@+x@2P!_$@HRkQ(&D13hE;Pvzs!9s2)1|g8DISN{CyJ92@rA*)y0#XJTa|0A%`u% z%S`A=PU|p(7^Aq5u1YeCkDIMVeeT(N<<-$=|DLkGro~o^+3|Hj=o!)~&8-RNt$GSq z!0Bp1cn46AnqC}!p=UQ6mKYuqZWsIr z7spG4FNnbq7b~}*FnbMA(F$EV{1&brk8Z3`#>bCKrfv3(_e&1{9-L$%qFsJvgz4lI z-oUS4otC_H^Qe#ozmO?bs7EfNDYb;7Hp#~}HJyt0}y!V&TJixYEz6Gdo1isjyPMesv4RL6zK#qn>#3wyKwo|)!1b2~)6?8B4| znALpLF{&osndk?RZup7kP8N@in^2o0zr0*F;E8rflYWYg|Ayj6v=gf(McD@m+kcToi(DMI+31k^4^V!X&vY|?)8WY8%xVtU1!3JW`8%_!KL zM`is@bM=B*8WyXadu%NV6{fDE6VKytZ*vp*roiB*kD46dj7ACJ5$!meMh-*ANuvx= z<5U~as*@vM;4RTBw-k(T?Q#G5fEw0`f}k|Si)w}od81Dw@i@vNje7h60e_Rf6Vjd} zZD%tv6=An_dbR62#;TH~A^}NB_Vh4vTwm!Jd?Y#VuO(!< zem-NhVS;&jdT5|EcTag}cmeB#0s?BlX+zViRd-Vro=u3I6Kdi8MP(BcH57pwM+00y zhU0pk&zCf~l4+faM2Pee#YWw!d_aDkVU#4@bZu;$SU|q}`=YnFMlv3}BN4rNZw&xt=hf90OXyJI%+ScZhuQoOMerif19^K2p8sunE zoM}U;!uxqNt}X>ovhJ<8V**jUl0NQ*G--CbwWf7+!jCT?;L^`sn_HA2tc`ISLX zYiXrYYc-z?=H0#X!-7YPvni%KO@7<9|6#yX!kfzE+LXpbaGf~>_2Evh)7+m?7#*Qod z`@>EkzO*hw5r-3UdzYvIe+46!}A|OTeiBnxv@zfUM!-& zyZQCAji$8uB<|<42YPXLE>n?+Z;Qs+hmUyPmWBB}^}mOMgS)@SOmrIlcfHAO21+u4 z#B4*v1D7G{a9pGx5N@g)*y8KjZm=}MB>;5LH7lNDu?JIN+d1G1svVPUu* zB)+toDPo>X>2*x#jHh!tjSZ$BwoV~!I+s?qlqt9=OpTA|lnsU`g^8d;_*}MMYd6?f zUj5!l>Ymk>fW1E02TBM5TQ6Dbz`$f`yZ3b}=P(Zqp+ur8F;F9UKLZeJxhx@XnbHL- zn|;*7P2~#hk8d=xW$^qv&~AW&kr_~Jd=6CY;x7TjV6-uqVgf6dk6`Wr9#<+>L`XLe z=&CA=w|Tk=GQX-Bsn;IDBVsoW@&Jj>5U5bh!OGFL)C$?SHddXMA6_SWjT;^Jw^f(7 z=Sy;JZ-2`MB4fps>$TMZmW$*GpkOCw`QFVu(XQ{C2cu_|2tdTJ6zEK-VGhJ2`Hn}E z@s8~R)rW8DCC8Uw-valIx2*Hl*cV-}+J$(p&ODl8Be+B*&(`4W7M)6m&#i^FLH|RZ zvuTqdurbVnYu18>)8s2=9av?RWFV?Str!UbT8PibYfL0R*S#E;7Ti71QeyYl&ERKV zd^N!`;ZbaL`N(OD()%=)cfV^^X-+Xg}hhC06i+DqE7|E(w+kEgvar z@l}dSpTg`Dir}HJ94&mu7D>wO8bE>;H+E22srM=M(Nk@ym;@m{TPg%<#V-&Fp9>>S z_d~@D;eq!63{ph6D45XMA&VlypmsZfM-x%Uvdd6u! z?7SKebibaK+JWy3f$~tc(QYBvW8%{E_~*Amw}VByB(F>$%8VAevZw>FP8v|V$Pv-WwuT;N{o|6c{FRW8 z{aiyuim-2c*(@mRY}_qns1Tk@K=G$+s=JUkz|yaZl2ZIZA7(x``P66MpvT>}Q5^>- z&z&jyr!v3YzkRCP^nKv~S}nh>|MXrbpJWmrxM8bsFZlf=;e;lKSdN##wEkEiwW1&- z@u`j(D9RW#l+ttd{@n{R8<#odRP*th?6@+hp00C*CVR%1yQ3?pBNC7z2 z;KT3)p()@nT{a-Ud%ID&;%PuW*abe=jJqBPeRpaTs&k%vzT_*s&O$}botvME-SZPQ z7L|-O6q7ivUZ!a+hNZX2876bBHyf{8IP|&1wm*Tf5*s!O?XmifULD6Sw$gsN)88(` z=yM1!mFAcXy_R3HkkBud(0tnJyG-xHE|fH2g}riEltQ{AkD0y}6sXBU@m1ciXjdDG z5**YVMku)Mr*qq9c5O|gjM!YC9~{u1Y^!YMv~EWQln$a$dM)1#8+mniC~7ETDVm;y zfON-WoZ!+R7A77@=GcQfI!AeFU^1cjkC#Nxf@?T{Z7r))-N?zI7DUM4bKyWCbMT|M zDhQRc2F9k!?mwC*nFfSlXZM0TYGAu@nHKZH3I9B=VswO|)aOot3U58Cn0}QG>OMRS zpq;1Ko+wM&SsiCN0IKjzZV!TZJ^>{>FYPbGj{V2X;Ppm*$QAV6(x&nBZd_;e|^!PXK`D4FP>t z?%qAh`wPiu59l8pg!j~ZuQMO9&?+_P_V?%*I^7x{f>n<6zlmu?zPa;)$%7o5B8S+Nb58pN?jD*{9%Y7DiPbq2-Go72$;o2Tffd`AvY;D#}RaW2FNAi4z&YF5sNkR$Lsf?j?xAD_AAlzm#GOf zOuL#9LYV>@vViG*u#G!@qpJ{tO-_QAIeiE;b6iT6yr}>zm~~V+kiyZE!L%p|>ZVNb zXTC2sE`fLTTDyzgK&Bc4`vXj*T;gS5vbBQu(x!S9GzcGXp}{mIG+?in&Pj^PFYjK8 zXr7Yb)i2@w(;!4*ZAL0bzup`M-eRKd1~kD~Uxsb}C^6X>&d|x*@8x?Sv5>>c3-ybj zD{=9?+M?&Qo-8LzD5jSA0Ia%T_Jp{e)D8Y_dfZPR`LO;SDmdBT^IB>d^nD!=r!dtA zs2IJ&U{T5wF}6{i0OlTI9zY!0t!L{kRX@PAQtK^7sZ%EjVJ9EQzniKW5OrJdrGV#@ z&-(C24y23yJKyNDo{`#Ai$>-Vahknv23ml+f)4Lsj!X;S*XqSn2Oalz7Zn~#D65p> zBF~ANbXE_=54nr(^_4QD))74sG$NJqUaHlZA=YJs%a(qreMvf&N2UJq2sr^?@*gv8yoKDpYS zdm+N7%dwv3IB%~6?Bf9huV(}4m1>B z=pge8bPmcet|$;Ng|%q1a+1U0Jt0B_NG!bd2Ve!8b6M;@R*i_uSC=V0bBCAIM24F_ z&lXTKa;CxX__+(>(N<#o@D--li7w>mfmpWoN2`K5kFPtcOo7#KlI&4R^Jd-4H_@B~ zN8pr^D7%N0AbocEd;k-CoiW|a7-IRfZ{^yM)mmH)WG6$)5`LVgZZCQHzt4LV#f3%k zOzL{NGK{=i6cY3S$?|K%{F_&GAU1wla|CNh-nfV+sDAK|z+r?Edf(Jsj`CGO``6B4 zLZ!_oGHM4M)UHRao2&E0tnmr?JdelihS8eCN(CNJiFK2QCRE+LNG6U>vupVlV=Aen z3t9BoB!?weXmR0XI@bBm@n|Pd&${Os19NV2^5NUMcZDak|2}3mG{dU-QZ?rDPBm=l zt#hgXS_+&{`;bC|@P{W{^gY`CO#W%@#lVdtPMGp4rV$r@!4U zc^Qx|i)Qs{o7NAC=J>_L6^DlsB3;sOyAn$28ffHmd*m9^rnWQbQ~OfLG3U^77Mt%E z)XgBNm>RPMoc-U!ANyF%4?VuVCuZhZVO!SuU`!gM%RTQ9SoRgxZh`BXVw_M&&vpns z_8)j?r_hWYBnUiJ5lql%2%H{rf{s{%F{*k$kKDdQs$3ef!i$W<2X2+GYd{y06xj+b za}N5m_=&p^I}lf_za}0Sc|1uv{8c5nLy=Gu29=q3Sn!+~b&^Pj?M2G3rn2yp@3|5Yx}{Sf$ba{; z`C>xw1>|)V*DK+01iv^cyYhq9b$^52>LUwg)jcmL6vUcSG5;U_+!PmxF9Pxrw%F8E z{=Rnx)I$`y2g&Y;r}g9V%;8*W<}~g^Z!T6sU&W|qeE-P$=k@+ghj0;eY-)meL1QU$ zzUNi`XN=Yq{g@zTr0ys&G>GrwyAZy$v1$NIOeDgvafyM%F4bH7tYQB61UvPxRMZ5cvF?aGpwb5sSM7lJ`Vctz1 zT*!dq9Tk?ofYAMo(+N-^OwlkO;kvK6o(9URLoi`tvNU2Ib0|6cBT*f*q4X3dN7AiP zWNb2vOy9dp_usj<-9r0p!E0z+Ck|;dcAxeJfn-;P1HBQI>*Lit?nU>{?JH#6Z18dH8EnTseE@mgR12el>|c^l zlLRao{Ce~C=<@+o_ERv!`o}4KPC?JJ_hZT4V1s|*i%{%X9n6k*Ftr3S*J-25dGUKt zpDrwLSKH=x5~HqAj?}KkV)Oe9yk8&9LiNNsa@1wfX&8jQKk=W2^Vcb5pUWi0;rC$I z&)sv-Sv^lF#^T)L&6A9#P(mLTpW}Rh9R`(hNx{f55xlMp@#mq!~v4r=)=j?N6^8&S#`!IrgF zaV-0fF_nu*X>2G@-{0D{*la1+wGm*tccGR1kSa-hg2T@)C41GVQ?5wo9I+bw76%u#aOplw1}6w zlHz?+6QW(1Y8VL3yraqzrWdBCy)){ae$})eI*mf{_3C?3R(W(=94f&!d6)TJ){4Gm zzMa9qU#RTxw_CxoVZVOuQ2JbSHM3A{uhRY2mEgKf!=OO@D#a|3?sK&zzZ-CZ(VgV( z;B}ZrbrH!`v&vySp{C(tOr~si)QV;(@{~;I03D9jNnRHU1Aj zegCtp(x-o`;ABYCO7C%s@Xz&_1RqM69{9e0Dk$RW#vlDt-~Flb2p(E|&-Pae4-Y4{ zw7kO+kv#?#Ge7Q2IeyFn89L`eL(;;NuC{xSaybQrsLpl0ZK~*`^nXQ^zk*J#=AIK$ zum6o&HtiAiB+d7(HY9k2C;`WF+A;VVU*q9)UKWXjrCy*q&E&mt56TqMu|CgDYW?)? zNE!|Q&cLemCz zt33YA0LJs_D4`c*XH~wp%u4KxLxH16gu2Uoc`<`jH#V>sR@^-z-n5GqrUj*SFZ6kEALn5xbGFPp^?K43-jxZsC|Pm^P;E!V1s|4rnrZPEQoKUWQL_)9Y?@XLy#pLgBc;B zc=95x>4BXgB2b|5$(rygPgNzzk>q%vaqEY_s4j$x=ha2F0@3#~e-o^&+BhTxyr9QD z@F--4Qk0U*sLsHde>%ul4L9fgWjRjk5gO%Ik=AR)bxU*vI23#uX7efD(9W&dk7cg} zC0ic#VC=g2k&_;FX~OSwG>5ZcQJ8XAMAzY1zV~7yied{7ixA>bZ~cVsMd^Fv*fE2i zg<6Z_b9u

oC$KK~M3+-$~F`o*O1~&LBr>_d-_VQCNkA0TB;ujUIm|h?*iuET=PQ4`;`nN?zAJ1i$3u8(~2(aTh!Hp9^KcQ1)E!E*dS zcz*Orf}!_$*a>A<%@_NGQ}=fWn~V_o>Ju`&3XPdo-Rf7a`={51OBc6PEn=;>Uwhs? z91t7`e(r*fB$%$V-+pb^NL*=}^Fo)aS5GrqcKAiU&5LJ*gt-*}r;sNabYb2{CEQn8 ziU@?;>;VYy=jPv+JNAzCvK7U42zaz=+(!tDl!OERXVD~BzX%-YOSk`e86-(KQ9Ksm zo1Bq3#_&^AfL>CtR{V8Qe^I6w5_sf|hQ+tm=P?vTNWrK-`Qcfm#AMf0o~^&gAoeql z*#XF(^VU~~`en-DC;C8w2Dhl*dWwazbhigJCoJqBOs<$4@fPzZNA_JD0e0}}1Evu} zmcY;Y5S&8a7x?}sSXu7^Ohd`uNYP?;Mj~op0{|&A4DteVnZ^PY@d(k53>Cf`-mEE3 z{k*NcHSBT;Uw&xyXyni zO|Fmea@Rov4{!?w(HqZ~6!^CCrK)@EjX(}Q{4RLCDgIC==fRpk9A;72*GSS{Lb{BX zexe=_Y}tp8Wu;mw4g7GBH3%qC18}%(Kz`$+-mEZYxkCFJB>XLxLeF&>q;eOVUVf2~ zM2*7{bs&2ju2bQj`_a~|Ay)2wBOIlJP&aP1>%ev}nO*)_5YhKf6EHMlUyE$B__a4I z%iul2DkrX||5X*^q>KNV7cLgN7qNp9!q9|zbTfidQp=Fm7m~rsdSf$Lu9c7)yCbUm z^HJqa2TeEnLK6=C8(F%D!`83O0Zt>;gNolpo~m&`Fx;u%16R?f}k8i0pN%{MT--gp_zEnKTaOQ;#I zruR=)UF?WpFdJ%UyB8=7pq?Fcic~V&rC+hxe4IYBGJo>(f4Ud!@W|m(Y5@@?5b1Zs zmUce4BKSuz;6NkI3x_uL4mAlUh(0Wq2?JROSC979XSJELW?doAGaHb`_7+Q}3yDq9 zbih$PK>WeedZB9m{W}Az{6(6NjVz=QwO2)a)3`Ts3o`q;-#;lVX z;i#99B~(+_LbqWD`* zbruVPe<XC1F;k#g;M4|-`cn6wY~4#$ZK~kD@s?~o16X02;Mp|I{GfLz zLASK7G0$1@S3%rfD68TnP-{%@>)~gwP?QQFh9RF1vmJf<67>RB7aVt@t$pT((zsb0 zNo+-K!cYeyXkNH+L|xNod5vXs^xG22I_w9g5T11D6kTgp zHOF2ug-5_mA&i~I4%g*g$FEv_)K(z?je=72QVH$H+$8h$zpKwG(f_T<{I{t(DGyqa z`diT;?0%(Jz!;q!+)>W8CDFJHcztrO$ zXBK{HCiF$6DTZtO%m(r3W#5ApGHmB9NZ`fd%*M1y^mEi#t4BZznx~OCYw`X1dzWRn zI0hWsW!1oGdgsui9c6P}$GGOrY6=8I)>e^c!8lJbbvto_scsAnQ0G<=$RH;9fu#%% ze3dNXm^fefl zR*{n7Nbho(MGIUa%B?!o4fmW0K*E@}%15FdqCj9cpcSV)86z{!eBD(e-ogo*GrM?u zq0TO0%VFZ1ED(!&tX{|Tv@pe(gnss}OVpK2;YO&y7jG;jME~z)rCE*};d;4FMb2yIeM3=kD#NVOCwC=D`Z!$NDQJ({T%=$9G;vB zV=da!|2h#UH)NNT-Slj)nB$s_fg34Nlkp7RD8>6w$GAe>>2J5~Z#q<(>PKG8QpbXC zdRB2#y7-}=&_5lg+P*1iMs$>eADSM2T+7&l9gsA0sm-r_f}a4 z;$k=LPvA&fvJhMFj0yLT?u?=~*av~x642#6}!d;y9b;lruU_V8&Vb>{0&avnZ^JdpYY!AA@V zF57AJ-V}l|i5*SIUG%-ifHVt1W}7zy6(TgL=19SInJ#3Eb<~L* z%y3Qv<)|g^y|s=Mh|eDxy&hnBGT_^|T3Re0|5)z0Zh^kz)6>TJv8S8kDCJ&r6*zMv z@)xiMa?*9hUejl6)0;aR?JvccY04Zc4bAB(%&zZ-GmHeA8{TrMFK;~33N5jq=on(% zt<&2P-`FR9Aat=D#y;Z7i=xGDp_zZ3Ipj#l`Xv5DvGu*fY>UDNtr~q|ldtW2cXb9G zKBnr3mPzM1sm71kQc3zPj3l(LHxTk(=I=!h-@_c_C zncJ46iX#XwXS(~rTWW|XEgo`UJ6A;-bD(WGvv?Cv(b7yu)c{?RuCnEf!vhJ4oG?a~ z1d(aO1>1fbrX~*LrdyxYEG|05dIN}pzEtPi$*1(%Uy~FSJK<;m@!U7T#CklX`SK<- z`Af_1ggI_80_xqOx$>qbgt*vOD!x1NDrewpMY_&jhZIMM-P}>JAd)6Nd=W%uX~Lkt zMkGdr7v9z_Bb&kZk?d8!@jPt`!#BPSam=mzf{q(Jg3!ak!ocuEVW4knmm+tv4ykI- zJk_IKNpg{As@vBdJu4rnSwpgY2a#?B=r8fT*Nhi+7#fmI{Q|3GPjW;Z}SP{?|T*uQ=#`zrk>ogw|uz=^x2k%Xd+3ret6lm9(^?fd7%%- z+Q_%Wxp*##A%Ke7l^1svSkpXxP*RVoe5I+8n!nz0xsk?V`*5onb=K?)HH#qOIE*Jy z^4aHlkNa^v^45>?7;39=*vM%4=QvoiXPyg`+ZqXY-_SELOFntUybQgg2bqs4=uz&s zhi;`Mk7kP^CT&NaiW$XZ#wEP!0i1W70*S^|2zQ2UI0gT*ZF$|xgw=PqijP})r{Wf2 z8@}0Hb|y`gx((;M`FQAb9}U-2Lz9fZ6~&{^nW0P#0c?8tOfH-?(WBjnctqgjy^&tl zU1ZB>hCRCVtDJX?2b=AoxN4_<~=eEBa;g}`x>d5wFtGB-*W0mPxbL8_i{`NguHCtS4%NH;7JE9lJDFGX>eZxql~D^VsFDzdjL z`!@)IZ#ym%K5pSs$KAivq;jkLU(*JaA^GN`Gyew?qw*Ra{e>a^;^zvTEo#Z? zuAQ!apO>KZ&|#-Ng_GaH&;7RuTs+<^3k%1N4)bEX<_4CwZ@Y_7No1zI}; z*WgyPYfr=&*jBEhpap=eAiry_Qjr1#g#?Y}c<-=zmi>xHmfv5X_@j5Rzljhu)Bb3e z;cy*p2Cy>yeJ~95x;#+j87VGVN6w*FE9fSqfYxOi=rnR`Kkz5pj8Fx&DCof(X-}KK z5h^cxeafHh#ro@Xplq8{$MxHAMtl1td_N%THbluHQw>uy{}Pwl3OqsC`~Vxs8Yw+> z0x&o+r5o9r(d)MqVqq^AJ}+y(9ydspMw#-%^>Ay;mO1jjT<)5>%BiwCvBPm;%(K!rR!54|4dNe;ES3 z(E9BeOWeI+ZZQwPpG$X7atlis{}+9IF6HO{Y0_13w9w8m@WXRWf<0}J7j%knRAPl; zSZT+I2Mb^Re8}AwY_0GMvwp@bE#+i{f26(v@%kGAE_N;)vKBF|`Et?$fCrE+L@r{( zO`;5hM>lt8>K3vca!peP*AB*C2VTKwL^YPv>G7jU>feJdV0{4BRa|jSA%`(hU4K&i zZW>k+5x=o1^kAa*J^(Kr!18e#?k;&fQ)^mD#QH-__uGk2TI~QKr8l)MA82Ae43BhI zAKi#~ksh|f`^g~fjCqzNjTP0wfuh@=;e!ZKa2Nw-n)dK?{cB`NrYMYbX&!?U-7j_8 z?Ox5I7Jiy5zw)A8#TfdR-B$_<4|&iBlo2v}BVYsO@lE2_~~ABaQ0br(uF4O8oD zu1~%23JJvXmc4g(qwFBt2s^vj42Jh1&T2|Kl!f$?Ug377MFTR?l0&PU73=oIB=6afS{q*s+i2-tl zTtn~_AO`OANTX1=8#)V65v@)CbkkEhywx9W#{9$lJl01yPh};`Nd~cT|cz^R5GiiCUtexzndqX#5#g@ z=O4Opz3#&?QJIq{BL~yx^0E=6YPHL)fz`NFI8&^{XenfFWUNPhKLvVtL)TLH@=0|! zX{QyEwC^fL+8_vymBdYgfRvyDKsQsF%ASTbu|6yo*GfFv51N~0&3}#Sxr|>Zj%`H- z&x2%IDVYvt_rA|BxB-dq|A*TeR{z4`CsGKfntzB-9diJ2*zW9t8bG8o3A=H6xG}Ri z6nBtld^~Cu7H8@A2oRTrhU>&0&pS$y26-YP-Z#B@{Sfmd%{J6lm&^Y!@R<@%F)hiH z;lf=99r4FJ+*G^aDA>GBnK5Wo_(g}ilsU*zEIlElb`}8b+j+ydv}(#8X%XCYBEA|Z zzEi{*-K8Kd&BL)wu|6*KWnftg%}%Hp6M~YEvJDl$mdIze0xjWgt4=f}uv&x-=le>+ z7k7+(UIL_U{4rxk8ye-#1^N$5=mF&guafv`q$Jis&f0)F&4i~&JInIHW9H1hP(Zna zbmuTF`*w?G_M)~#rw`LD7A9@Z>Y9bB`O8hYu!hB$S={_ah*#nIG^$*{=f-^;LcBh% zKg&$>xJHagu#gGn1O^5#;TjHsK0k~?+DnK_y%$Qpsw+;J-z|=_HPx$y)%Cg*S9pJQ z&h#yXw%G{f+V01M9mZZuCedq{m|xHqi^`1t&d&$%J)!yKUVucD2l%uq{1f>x08v$y zYEuhzS$|5v;s-;d-p$ob8h)(*H#KBb4qZZX52c-#-=3<&FwmJb``lI*L9N`31-{4KIm`@ z{E8|#O__!HBt8T+M{1$PB~Kwp6!VxyNa?k_->C#o40P5VxSDhx6cqaOTCpR^4NpzS zSMxOv&PKjuh^S1cJujNNzBdXu{aX0c=v1d&6LdeM)#_g3^l|lJESV*;8<-EfuTWfF zvdZ->!{F`33Q){?#MW*QBqDTc;zc~3-zD}v@d<}%==GyY4^#_=4qK4ZlO@^+f8h~U zaV@D8nGd^o=%_5PEN@2AWB<-Ll-$)pY*c)JBO__Z12qS~mpYfXGC0h|d$}}NB<@*i z=uDCoZ(^yk`_6gUNGZJ^^DG#mUHI78GWJB>yR1Gm6-EY-Q)KP*&J9@bNYc}#Fn%KY zhYs}z%AR=9+2JAQvkwCL$G5K9;+|cTnT?a{F{%BqFMvo%~y)Kc=29&;&R7DrdxA_3k(B7$bVv#c(DQe#sz$g)k(9o)naS&fc=nP@&9IJhn*3PjJOcc z{}FP6NZ($ca|0fy%@wIN#{a2IrNov&h<9Nrq)&GUAd8D*8{561URG%!`1C^AM@hpL z29GTJLZmpQwZ8#Y*|Znf;2$zb3#rt}m$8|xFM0JL3~D;?wDD#sE;CLsTmKU(+~!0Ks%Pn7(>y(}iE$ZQ0R zYP}ZCKYXg%-+(Sl=YF}Kq}MVEXlJ0p57_OxDXE7@Uyc)c=si64Z?IdKP50PH+25;btt@Av z|A%oaA?{~=2s&WE%0j$b0DPa~kBA}@lRza5BQO4Kmw=x!$(=9)i}v!O`g=fHG=7;EY9BKe&c~ZG-Q(=rJ zx!bGXCm&Nwlm3T50gk3SQ2QBba$Fk(Ja2G26sM9@8lnODV*71Y<=24FKBy!~ZYs~a zjz6CBBGhWW&h;l!^Wj^o-K(Zm&OGJEO%h zZ5BMY%Ir)r&$J}c1hW7e*J98AlS$(Ed(r#Fc;7s2Or8P{>AbT4`U+4w&h-)~32uJI zD+~g{pKS;x@oJFtr4sYRzehGas}oEOSODfe>-ok<(Ct4zmmf+^QL*jUQeI^I-=3{F zR09H<1cICn%OdBkfTyjiq{gczz5JeYm@R)15N(+l%WVGp-Waff->@SUUTnRZwSFVT z6va|eqYgrk$uA6R014gHtgQ5Biv|t?IULZpzxnb1Qr)7c<40;>cKq(|XW{}3(z{%sB@jAwD)4HuA4A^7&SVgMJe|2AD`1PKqV9AM-EfTZKC zTV>n3KfIj3UL7CbA4shw^B=U1!xnac$5Wg+(9sW0uCI4o((|`~)H&n_k0^V8cN+<4 z(wM zA8BgSkL+J805bJB@NrD@@!|ievQhs|>wp-^=6!Xl)Zc(38@@y*C4-xw{_x-W2AHQ* zL}#nVN%~j}3}eh?^s$n^u9TUiNG_GVJvg_KXyDK ze4gHX04R@xDV%_4`+qFizWuV07Zf-{X!PlZ*upnpwPC9j^FP*!B>SI{97`3jLFzWj zW-R|5@BPgQcubTTf4H9i-g>cLCvYHkFjQ-{(cT^)Tl!t8+O!ClqMiJ-a$qpS2EZ1V zN{s(Gxv-mpvZ4t9hTsOE=CQ%1Qed|7B3OkHEe^e?%Bmg~)1K2vMm=luJm&)MNi%X% zg8%RM0FFOElR@&ATG24uk4eNPf(@9$0>&)+Mn7W`;00{|PQCpx4(gbMPC$+`V*yp| zCM|Gr%Cg!ql91LHap1rE!B`spUw`{}R+}_$g{(#6J0$9}Tv1 zc-C#H|30GUlG)|ugU@WHzo$EoNwauc4Nc^;oxg{^+8MYli#P8P?MpWsfl3#2^p#(@Y@53@z( zf8P)AKCeam%uT`2eWa)S7CpVwi7!`JPBg z7faRpkEKc=lAw&jS5ybq?0^X7 z1X9FgzzR$O>w|a>cFNEn3Hh%NB&7kW|Lo20DT7M(CZ|X;-r7Ub!bg8nZDdI5GfU$r z{(T<65ip#B!OCYU;B0h`|DPmA8ZD+}`75dYGZb=XP$)?=T8awc5^=l^OL=ac2$=R+ zZzWv*VMT!h?tepjc(E4!=?NEW>tO)M>SiR6RG7gbEiAKY$W6D|Z@P7La-OQ(7kxD} zS-9FZlEytE0J<54WfjZ;iQ-4Fa|)7tdhL7<53yx%D{K`yjZs!k0BlVK6GD>XSAULc zfw&=<@OjSjNtK6ZFApqpIxuT+3|sPsr|?nSe7`x=zXqN2cQ~WjUq50mdO12X!VP{>PG-6 zrs zjzi$6amPe3Nx7cbZ>ypbi2AiosOvDA>Y!(Qp-hCedad=rG|@(JUX+XxbH)ESvw~M} z+($=~a;+fODe%O>*Bn53K(R{}=N_xVIEP+-qWUje1a^|m5Z;f=10vzA4wwOZexUb> z`7!Vv1Sh@}n+GX!lgR%``%y(wIrCJ<4=d^EeFb7%7P zJv&&@AyJ=4YS+|72DZzM*0=vefgh_|c)L@fWO}yfX%CaG2Cp%W#2=(aqs_rXS;9-Z zfL<77dEqHlfnNb~M#XVR>T~N2Fwh^9psxVRA<9C>wXYj-uUObY>xP}bXS2;#Tf`Vh z%*ya9v$H(?Z%~C_Tj1JKhC^)v;s+_y`L1_43eu!X|Aens?EJF>58FACXOpjG~- z6dp(8A3-l32>#Fo{QN(+=notIg~?l#%FZ>|4o%mXe-9{cehqhfC0~^J^NmrN%JMOaW3fjZR z$)J;K5Rh-YJxy(gasFC>-y58yL2#UPd!DL|!`#)spISP&k^Nsz*fB;I+bFGG~M{b%<}|+*jB;Mh>sR;`lmXv zwi0#h7&Y2HsB3>~1;Nt{Y}I1VubQ7@@P771q!_E&lZeqrV%Ux!DF;wg7?O@t=5X8-ap_7OA z=f3ym!ys{z`u=lsr>-8&aA&aQCnzo#*?6q~t$DIFQK7%S%6S1Wjd@d6_sO0Le|R(; zE5%{l;}H+;S&O_?>uivo{r4foa3b7Pe7>L_i3Is4@nqZ+DNtSPo}H2o`62k0pZyyB zTyHbOaf?uD`3Eh4?F0;Ni=+^o#rg^6ZY`0_lfgJ14WOTF zfQlHFH8?|0sKiouvg9@1YJPqW!f`SfWSFbBUs_BD!xy}B3bg^SaZNX>`vc#5Yqfg+ zrhj(5Xm{3XVUzAR$ypF7ed zK})7RwfFv8Z-5kntx&#Y39Nkru+6SxqB9x*tenq_7TqbT1@5Lz3eBtZhr{`PKP|JD z)5Rfj)@^a#Lg%N`g9Ic7V)GTJ_XQK2bWv@Ajdko`3arAZqeQ21AN}{p|KsYt!>RuN z|8e8s*vFQ6?7g$HXNZgvWhM%tLqt}NW0NgpMJFpe_x(_`F}g z-*t6e^+(2eJ|E+Lzuj-+-~v2y^ZkKsdY2R6{>x%}7tq$i?U|15cLMSli9M+>me}xK zoj@Q~jUX3Vvu#PxUMiG9JtM_H@?C4+?>@_5Acbl_=#1fJl01U10` zka)SOZP#9_BxY4BSvTW(xnT50Mx+w`?^xvwaIz)QsmlUg9vi?ng<2FZTW!91womc= zq4fh0aI$Hsj`y}t_;p#8@c`6rJ;hrwH*)}Yp=;CWt^S`XZC91H+{AISp`rA=5LI{? z9In1&WhRGOCs5Z9)Sw54g?G>NucEeMV3mFoG}EUm6Y8jqdB2Xw$?L;_j6W6lCq+<5 z({#t~_+aZnE)bhXefUS>%VD%0`%k?{e+i%C(K zI*nYamKo_D?z!OqwoE47Z2d)v-ajWZ=g*Kr5920=X%mt9ks1at{;S!}xwT-|)U<~& z-k42Hkb1o1_@&WzE6g17E}X*q2G)O5`wrN$;~&AT7=E;UA<8nh`s$^;n*t%Az-vsU zi;DcSU-={x^f$uI$7Ej==6+R`g57l$J7=NZ8frLGh-O z(FzyocMd!;r`E6n1%VFWx{?BfUDV8*BkHyRa92OD^_vSfh}DSFdNM~~YUo%1qq&7mnzzjHTq_~9_f@^JkwPQZhu~b|x0#W!f59pj?Z!l- z_x-kx5d(XA>TU*u*nC8y-lEQP1>(UsG$lt3Yu8`~NqA$n!Jj~fGj?JfP@fwI{3}p@ zM8njz_xEO2)oekwd)S!8#Alr6on0U?UMka{N;q7IT{rBB4M>2hV&tD?<@l-&JI?ye z*}iU{fy=`4H(M%HW7F|g$^lZO;EIfYtz?A;>Z89e$(V4wcB?OA7FyiL*Sfb;VVv$GBS6%DQvJ zOi>_}p)0BP2sH2YPtryi@TpzOPFrsU1`$t)x14%CTraO{K;sU*-gTQ3-@c|Z_vJaF|?lG~pbSMuav-^@2vhHxQ)GJjB9EyPY;^dQEuVCfXTNvd~*&!l{`$Q_OZ%dWUlw1?o1udI7j^n4L1ckOT#f`3T`+VnD!0+O($wih8q)U?$6y-_T z0;$C1bS9m3Hb6}D5$vX|3EN6rm6Zu#5ID{64pN)t#Iwlud2daBXUhLX%M%j7c;GdsA|C&` zakJrmbdt$G{KS|{JfR2r_@8E`KsoTxn8|n&=qFcT`b|p!0odMq4QQ?ZclViVglu;L zp2MX`?9uLuMW^`nyU_puFVVhq3Fs(a%q5Jf1|8$}t}vus78bo~MqxWza;-aBNN*t= z^$M_HuMSnsMX<8`f$Q!i#kL+1lfcZhX&`(D~Vb?5EbgiRB@50e_ve-K61W(mI% zOA21(SlldnaiiQyz=qQFsV}v9lZJuxaLy@^TZ_1*=yk{dI`&HHF1Xgpt5h*5I7S48 zB~R1s*_whw00>ZG!j+X{FOyriU45&fyWLYP4S$$yPp?X%?wH02u3|pzMk_*X1bm z>bqWKQ+Ien5>@!i!|#d5%aE9toJoA|wRRrcefV=)B1|wO%6>fAaE~TmiB^bwH4<=^ zh7_+JZ7@mL{)9QtZK3bnqdo~}4;RVi^!@nD$*(#mPj_6muimIkZ_9N3`5w?mo#=>f zAi86~UPP(@qBknU1+eP@hm^m5TVqKQm9^#8X+pnoLB z;oXUeyX+7Kx=Lw-=Xqp+-eZ=J_4#OXBfFjRbyO7SgtoBSmL#7q3%B`WU-s!BpA~qh z>W<6r1^ixHBK}HS{|@oh5sAD4j z4*CH9y6jpPTnq2RL@xJd#uqNTiV}0Go&1!k6LVl5&IbaWJdaDNxGmUDQxqISMI+*qxmp)_8^B$oqbNrMa@D=UpbB|QS4z44{JP%T-@45}HTg6kh# z0Dek}fYfvLv+0mmmi0i`vrgSkF^m=a6FZ$29d{=8EznC1@;_+cp1&%Wk1VMf$rs4+ zHsf97|1Qv7Ua$v8oeu-7-iz`HxWBCX7QAF+{~fjfR1`uG;2t!*+h*~au$>&dPu`F# z(#m)|Yu&&1%HOw3GEHk4jEnX{DuCT}^>?p9fP`>rZTy_kW7tIsX~4R()sIH9X^flR zEc5u1w9Ob)VfJ(ur*%n)z>|9B=mSd|O=!ZaM?;@313TRBtZpi9{xTAB4liiY#?072 ztyv9ZLp=cCbCA6QhbL;*p(ybkDYtP+8T(c|HNdsH#}a0;{i2zI!0hr=1{|hK&M1nS zp%L4TsP{EOjuH*RKvz*=HTVR=Ug@?EFZh&awNofbBq@+_(O|NsO{3i*zX1XI_JLW* z4e71xsF#msM(7>Fk`a{e1aL&jKKU$Els)ge6lS7udasC8` zAtpZrFvYTB%O76P$c^97V_o_3wwgm+w}j22%<873y6#QexaF~;4Q$f_GxfLQrMVTT zJ(qTBwP$OW3vJH3C_cBIJft(nSpC13mEs5y$3^4i_xIu!j$_w2eQ^%A9g+K z8D85H_-YP*gB=9IbAo{LmY|m*_mbc`e=Iokm1wgCXafZ%ukOai;$hyv*8)*_`{0s2 z(GU6u*;O}1_yNiM_nEyWbub_>^>+CzPoP2jDb%kXOr;98JY@d^XVh>@0^g5&{Agr! zHN>Pg016?)-n8*Ybu*8$9v^4k``C&mq!hvllXb8FVqC>d(AbIFc!jK(&zz zpv-!8W?3)~!>HL%kQJ-isD*mCAyRbNiym9^F}*>+ILPCiW2H+Jx)ML$TT+7bTpa zi9lRwtwjf4WZgZ~uiiR*%+P9-t-+q0DxpKRf+vQHfq9+`#vW{+PJc^TZ-++wLt1;0 z({BIKVuIh+Dc#_T`!5r`rS$z;Dnsl98PBGJ)^k~6RMbBvOAWPd*hcRC3!(hOS>fmu zT1~FJ%>fCmk6|ZN@d~O4EYffr{UOCy>(}n7z-m28@fUsyI8la?-rUThI>@&@4g9?1 zFe3^N?ock2gQoI_i>?y@3w=FR81=B2ZoPvre+-lt9CQd~97r=ley=i6>B-^o>NV*2u25`x zyisX&WIGdB!nyLeA^_N#U7aRPMr@+abYbko^Pe;@#F=37qdWEq^|*9~hVkLkmw}sH zdN$pUSxFkTHA>Al)8N`w)+dOdhXa;?>pcJ{a;wig#o8&=x77%y>O^gu#=p0qRLZX2 zdwo92z54)Lv-Br3_#AxhkHEKWJ)U9seML>5@C638X$e!jgMr(ncQ=`E#)}H&7L~bI z*polU*CzebsE#1GtJQ*>@_CX71t?i(uIz`B6Bqm&fW2Nm4&-{8X8fV+;>q3FPq983^8S0y=`8VE zIOXaAMk}{AEGu&dzJN7nx6oA1yaP)e`b;ACM!1GPF;7t9pU7J8{FK3+rT(0#s0KgJ zUZ7I92TFoDn2W9CXv>o)GDC&1Ec;GnoMPgwK@vR{@Za>S)e0kp2*O%QISsO^v9D#O@3f`H|GwDbXC+rKaV=8;K zkl67qzn?J>5pX-rFUcri1rvN_ks!1&(>Nyr88hO2ntS#_y88lV5OC}Fb<7WY@#X;Rk?~#N3EAXjg<``cVx# zVz@`#@TYbG#R6jn+Jm5weiR>F@|B%Qt3!(-@o3!-Cnj`RQJ|ZUW(1Vt^LGQ%yI#g$ zawW=#kC9Goqa+pqjG@)hUl#9DF&E{Gczc}@I4RaYUS2I4xc7A_`Gno9c9>&I?OMGq zSN8HWdG)0+c>*T!Y%X;K8u^^VMzu zs`KI>OD9BZTSOqdLlMmLh2_U1UhwANcMr`ifw&Z?2!3e9Ds$Ixkqu)0){od?v*rgp zzh=qazY&Ian`S;9@DsFGTh;*S+oOhCd`dLX4HUi00HOCv!EG64N_U#vWyapoTGX5^ z4nq-f#WDFJk+^-9@zj2a9`2Al;0b9@ zfR2j!lBBm8-MhABa!b>^oJrd6XLF_yt=eXNdsnS;U}UZJP0eA=?|4+(;Zfg!j>`Q0 z37Cavf8lBO-y~fnWMi2>P}ttcXr2uvpL*|D;1tX;<~be~>i3LGK{JDKTBe4oU2fAt z3tw*DV>9PNfWoFC%`IA-0{3r=e`h55G_yB}$1Z|KU$hY76t$|T9RRNIQh;$u@c%AR zlU^?%^e+I;%1RLjmFgJdS5`?-0h@%VSTUNFVmv^M>+U^P{@B5HDLL za%Q={18+6EI?}U_tR0Hn@IizERUD_<^iR+=c&HF3o~d)XP62~d?3;duuR01ucP3_r zpZV1L5Jfc#57{YD&oKik!7oh0H)&?2M`iUO!oWK7G*Xg0|5|C=ENA@nZFS3t$s}L5 zD2dBWU9CJC=hsMxX%q)N$#>iAxiP&b8+Cc>gndT5k{^V~>jXpZU>3-VNW%f2a};48 zLRf8|u~Y2&`?~cjFPl#HUqE1fRXvrTIK_Wx&3p9J`!2GV(Rj-!Jlm^=?0pk3oU@i&~(+Q@Zyo z$Gv5isu~br8}}iFsxuu&Gw`SH1S3gs7%V=+8)NH;1<$ZM?V+`mNDtb|iAo=?4d~!m z!&H`)!Y2e$M1J;V#9;#qWdj+^!Wyp!JCJQCLS+svp<&5kzvEJ*igZX3;iRpq)QuW*ST!WDtjY0K{a< zOJ~7EakB8muZqMerTw=AYb6@bEvF=@>&jSeuIQ{qRZY!Up*TZXO}M$lmAKg@oVW|t zoN{oLrZ0u}110i&ucXQ=owc1$(ynzG%1KD;SDkrp8vZVTS<{|x_U+iK7jPl zBy$COJoSOQVBx|8oll7du*m2CAxm(Z;Nwp%KPG*4@55YwEQ4Et4tHWAs3A$Ko$qGj zx%2_v%(7YMude~kuo6W*sZI^Bu2I0hkgTf09EK9mx@O~v1tW zcT^Dby-o|9*Wrw&j?*^lTwUlDM$B_nW@OP5kz@Axuqw!^PGQ}q>h$+9J-D8)hIrdx z?Mn@440ql$Z6-^UAxZJc=Ck`?`nc9vd&`?hYe*fLZC_a9^TsBgIci7(Y-j|^L zENVTJx?(@7C(DXvr|)+IMj_&2)$}WbCo4gjBqEcWVA(%Xr{@_aCCEO%)1^QX(%tkT;~5t_(} zIQ#TjTf%r%mC7uabFGh%JBZq;ju$tI!c!> zcs#fGgHVI%fFJ%sVcE0Sk$w2fu&TD^2qjwaBkJ;=%aI`FL?Y}Pu=?{o>kP8^F{nR- zj4$FuybHL@pIvasivF|Pc1c0P^T{S*HDkfKqozblE`-oP3l};=^ST}a3ox?s6@$9w zQ=8s5I=S10C!(d^B_&J$x!Rw@B;D;rGppB7feja3ibuaeTz04g6(dx@9V0c712?2} zG9HEEUxabbSuMgZBZd-L?f{~d;hG!0w8r8GdyaUeD0OB*0|78?N)N-xVe4i&R~=|c zedkYFrO^H+c|ix?>#QemhYi8c(Za;ah`mq1mt3N=yXAx)*vJLsHxk z*&nMJhnhS)wWx+`Xh!f!c!}##NKM{wI>PQ$CIcO!?q*aqkC>e}M@Axl1X^qODVDo+ z4rj0arRYolGyG5v>u!YZ5xs2n@{L~JAVBsgqsgDUgchMc8*EV3xoXKJL7{bFVEbuL zKZz5MVf$NBH0!+3loW*eriTuZFIwi&KZKV;ICdJ{ta7YL*lq&gVVt&U-x7o|d2$&P zm?3_n2HVYvN!lTH0vZK~OxYoXZGXEZI?=k@neXC=xh1#bW%64w4cy7PQNUtjzT?x< z3h?=Pa5++=^lFDr)Gfq*2ah)j;1BenX+-TezrON_Le-1Uc6|tjkwA@Bsl!5`(a|sNvgJ{FutE$9Ogubxu5E+uuua0KG zeO3JSKAq8<@4H`brUYnYzc~#eZ&%jtjmmAGRb{AV>2_ka9ibom;`PYI3AK7CC53m&FPt+JPRvU#rcqO)}q(BOXDOFJ>V_-hpmbVGsM zf}jWx4cFv?~CglqlvG_aqtHXvFnw zv0Yo?8eZ1Y^QNDxTcyKXlyTWD+RF44uUq9ke_DO|owNvkLw=eh2J_~Iv&mzIE0-Zt?y0;VK ziI@r`^*;3f{?kmuC9xV74aK~@!u0#7XeQBcqdGMAZYtx7G~AZHze1Sv;I!D{^*K7N<_{Oc|$$l3$C8#D=PTjfwFezeuT|+WCPTjf!40I`>0O>ketgRqbzFcxV|@nYUV7c^RiN&ryc{ z(3&6l7>ujmJ_dZd0eyj-MaC4yeS)mMk)i;nWe{eNjZ>KV11NZ&9?^5k=Q$_9Qc!TR zd-n$LaF`Jq&$#SwHeBmhO|^^Sc4Src9L<(?G8kM(AAXGOtS{vDBU@Kj!4)mx;MW$1 z@155W@JVEZ&H#IbpVnfY?j5EndlfK`8}Kn;u=<0oeai4;#wp!~@fBB>ifk@1&D;#6 zKDF@emJPzE?l6M43T3sr$DQD{7=AN5>X^~72rQ4pXNuqlV;E$Y+( zmCD_P!%IswL+q+E>*dG)6(?L*Lqnd%K$kB+$h8W3rPqJRoD+1kz{@UJ?JISM6C&Wr zfZIFAQG0!3`ntanZL3sIBLS+K^gHwXe!IpBWc~PHC1p80Rg@zBFBew#py{p^qd;pP zTFT(Aevu9rsYAQU_rSe&Sv>1qguJ#Q$1`!8HR5qrIX7nrjskTF=gL>4)vt4@FY+{c zX?O&tX4Zh_C0u>p!cjmnjONno6R6h8_!IY>JA{j!dCc0((Nj!^w_i4*_8weOFFm{p zqdp$3MN2RNArHIayNV$gB4wYUo`ye}Zi0@3=L4lzA+RtY}C^yeCQ;zw|@oj!#+!7N{ni&*5 zpB%h;f6fX9&v#43W+Ht}$U1#*=gql>r=(GDxh)m#|66*7tB0%z;4J5F0bew(y^J-G z^zA)#>W;Rz{&xuX_kG_B|D}8gz}<%umz%*DtoO#3K$_xIP@tx950fEg#k{5q?*1Yb zan;$TRgde?k`1j}`r;WUEFvOuYc7c}x&4X5FEu@fZ8gSYeu8hb$hSWL>eHebgLUf{ z`wh*p-5MTkxL$qZ5FO~H*M4&a=;iz1YJ+mdsz;9ALAr6FJXCW!$V~dUNlFYI2vNqm zQxewY**!Iv}T|oOGdb=0A|SG(;lUhs!tG+8-!^VPi^WOM<7W^I3(h9*iuE= zGiNfqJs?gyo$p+2m`LJG1ijzSBA_=JWJunJcNlzeJ8sZ+5l4KTy`>E~@PmPv9Dn-$ z?8HQW4BECT{#gkzRZ;g%Uoq=E)KzZ8n{DK65Q%K`mRVq}ePftJvk1VwjSCUYt8@*h^ri)B)XFe`qANk2fTW)OY1~HGz??xyQ`Zukn|DZfV1qJ<+3E@wG{Zw{G2LVRXmB%>@pE*!58Wb80 z)q%9UT*7nIm$Kncz~n47dJDqoUJ25ZAFO`qh7=?f6vN_)U2%JEtHkpNZEjdQ&(wo< zbyVnUDk@F*U*RZxSDxW6AtN+J>4{6)x^7

N7;1E2gasg-v1l)@QEBBY0zZB?4HIrHbc9eS$mwV|IT zM=(puClJXA1l2J`RanCvMYRSf(C1Qq0pd_VR&mme#1TliBYO)@(C3j6GD zf8@W7Jrg_uL6EqS}Oseo{Rtql-l{ z{wCnlzJmS4J+K5k9vl&ChqaEu0je~QhU6q@xxO<}83Jhu89)ki_N;*uasCl7&iM$a zw4i5@+QMJ5=q+G`Jl44Av_5!2gf1LZ4U&Hvi#LRpO z7a9>H`OG9L?D}cU?uk2!^#rIY1|Tc98Nym2imBwo94@^Wl1WbQA&tTL3j(}mW7R5y z*Wi=TDD@%8*z1)4Qv1)YSyaJi!`B}LSsl~9X~Ku{_+h*7zVhA+|s|3}f~GianYV@H=3so83A` zNPDW`0BKc#o+y0%?rzYx;a7K^M6}6l9+?SIC-p-koJ0|8PJB5BZf#q56~f<~F)C2c z#tQJsAR~d=HBA+S%$(c>U7yhf;7jA>@?-n}5YL_UcvdjAk7`Lglc+oF;3>?8-yKto zvQcnTx~tbx1dJaE{W@*1yqMxZX;A^z;qcWnR<RH7Oti(&^Zfmj(a!}? zkn=o#6kGD}I(Y{b{2}5y;~PTqb}DTF07}_ws^bVAle{`4i@FBf5jpe9R|)tffdX}9 zNSh8(x&dC>(k)D}TWbjZpn1s@ff6X=+oiMfQkxa88h!cul6z%#=@##{WRUDvS?Gw55#H;-|EMVe&ER1DYu-yQ& z=Djz`!C#@O_p|2Dh?8dpesEw)JNSNoP!U6aKbklT6{1-z4{}xvSmP1VOtgBvx_!!j ze`oN`Th9ehfJk$x4p^6ZYtxgV!AMLgk~f!}-vDDRxM8C5Uy)pCZYxOSzJm%~;j_#h zVc<>_0z@lmJ`0D*9N+*7DLg$AuY_Xff1E<5=d*r3D>=RuLoXmD;Hw{&x6yb1J{(8j z_+~|4OU=Q5y}0@W1SE+&jn1}VVkfwSI(9&nngQ1JCuJaJ6xU%Cxcm+^FSy!s^_ujf`>j8RhA-^2f6e*(u+-26`e zz+mG~hhxaD!g-YE(}~KDjGhy5Goo{T$k98irbb#o@43{S2pr_z2qeu6`q?X~{Jgde8J!Xi$Rmynh;! z^%NihkP2mWrlkc04uSdhh`eZl?9ofp_LtmiLk7=x0DzVWxz?A5U!y(PTn&p)_jY#q zTVaGY6F>o~v64n5Te1PDG_!yE{upW_P5w(N1++O#S3AwGLC-?(Kh|JwnpoPEDE&euv8_ESpdgNA-wM3*3$&WxN z+@R04aAgFk=$i*0X1vzR2VEgixE#os(4idDYy7)@fRjRQz{`L{AVbR=eqH!H9_a#h z*4?2O8hj#prP;|}1+Nn{eVYc9buXa<5EgVlUSS?Sh6QTg`h^yg$s+%ME?4ztIvlXZ zhO(Z3nfw;}t79El{G~O)^ojJA6xXo|_JOf~qJK(xssM-Td1cF=iH#Q^GvV%HPm+0^ zOqw+U=50u?EnkxStQ8E=TyETQ;k)MmN&2K{&~eatFV!$S@e5RxUR3&kRRi{RZG&z3 z-=|a$Apv&W8Zdca`Yyja1Ge7ZgEA@*lg~=E%%1nu`r{)G0sYSiK-rXp`aWwTQo!^v zX7iH(V4Z{8`Qlj2_6@h`QlE7K=cLkq`Rd{NA?d$Wfw$6|SfjRKKz40+3v;zGgW)21 zownM;$SmZYf5Es_&>-GnMKU^J{O%%Unx4=L%Y4Oz9n zcE{~fn1??SX7~UN3-#^e=h zwbR+}ToP5S1RkGtm^O;N?qs@iz))8bUKc@LRK^H-<33S77kHR(R~WP+l|bG6C3JF| zO8v!Xzjot@)^D3nziXrf&=xsRdk02iXWztYOd`|?2z2!f@H(k{bY=mC#>ZG?KNa+0 z8CK5u;SI!9JP{#W_8?an{AD5!`!#k7nD@As391bq2hQD5cEC%1tAf&OSeMm(B>rZt zvJa8~qDQ}UuYj^^8X>cs0g!|=tuqJq!JIMJ5J4VoL`)<0?_^;V_#jzZ5=Mc}rU^+5 z)1-Yp^)xY62FGY)gD}w?246bIpX)c@FvYW_w@R`QaR>|nJyrboP#6+wkDilvU~my& z!3TsTuLD3)JFsLkO+7I$5{vgx251D*PQV*}-%e$qbax;@z1YFf)iWo0KI-NQ_fVfx zLCzZ2NyWVYlC;FI{&zq{=hWDvd4YfwWFiaU(<_ZI;9m*|bc0+Uv&MgrC9q5V`>U?S z;H9vay^zv0bCT53yEGd@I0YSBVc5PGZ-$KGT7&L%;m}H3)~W4)6YyE={oXJYEo~Q! zPW*dDt(45a--QBwjKtF?2ma57<88+l!dKm7jw+LA7iAU@>(YBnkcul34wF7oebUjG(z0tCowh1p&Pm5GjF zShtLu#g4GJ#MHQ0_NQDS>m+b}pi?T*dj9nz^rHTLUk%h~5b53;1PXRty;6i1`~qZ? zLX#}qF%T-RMgVl?87J#@=g*%Hsh!SIQ}OZ1{Qa7tuP){!D9&g;m<|A6$=Sj9a1g$? zF*)`W3}uZnB)L{HcArv>bwI&k7(e>DnbT!?sY&$dM?p76QWMjf8pcT61Rlh!gA!(?OSrZwVwftp`uA zP#o_KyuxNJ-?e!i3K(IPv=6Zmn*kEFwNTGDI>3j`L>|OSp7>v3(vY)a5mPBghtMS7 zT2Pc#PD3O+FH&);P7SD?pHdnp@cp%Zp%381Nss(_)J}e)`2ZnZ180Kzh{pMDI5P?8m3fk(ne zK*>ICOOb!^^Q~iG7InrfV+mPcHd7~(R1C0@lqLer!uv)L`NbA? zQAX>V&A-q`X&cVD`u+Ks(xBMqWt&pjNnFrY^NiQLLl#bm%r-#HVdrH z-Q2RaQX~)W+leo4_HKl>%6w8@FfscpAQObtHhd4);d6Og_3zQH-iSnXUAX7RaBZsc z*O{yg>^VF7soX?QiYw?lpB5Sj_h~i$thvIp^a*6A3`sqv?!DK^VBy_zw(+2m?>i)? zT(K!!kUPseKP~E}@a+YuZZ?G{P{5c6NWSZ%|4MLyKp*}B0bsqndMNI0Q^m9+NT&w{ z(P8&aYQ}+jZ5}j)4Wb%ihB-8rpa?m8I5Md&thG8`_K>Fda^@lE#Z_)I@wvYR-kpQM z-1!ZamznI>tNSLlGy5i7F;f0hb#EJU{N|&Z&xr8*;rbhYYS**Wz&>J{c^93-Ac#nY zj5p zcBY0wE;Pn^w{C|s#3hb!B-Cy^M3;U7J;U$etST{d{0gnIA7=L zjpBsA&x?Ef7|P!S+rDDYOnxCw#-BU6LB2$Rpz*~9jrwPJ`Xs1<8Uz}RqDvjJZt(XD#J=It>vm^GZr9#}=g6Nx%^9=bWJ<>8@ zowvgEN4dvtkA04UPW7{HH=u4`t(yAJD(m9LbL|97eV zy&iWo!S{O1<+AO~L;$eBX1gDnGc^}}0!ts?rpsJMk)+Llr&n4LlPAcj5#kURP!P=@ z^8yN>oHDy2W}ce+!_n>DbC*KT{^*@vuY$yg2_h`5_47q{@aw-9kWqi+>T?31ktv7_ z#{q&_D%HbXy8(4(Gx(r!rXI-6kI(#eHs9;@1XBcf}`J<&p-&F z!2_V?(n_VD;@EDgvAkuIwlr~<;+jja$w55`uAH*kv2H<_G3_ILi2H`_msRW9w9s({ zrxhwS$+@&VlTM zl)GVAGSr(F|8DEQ3%4a2g^k)TQ(%4nG~_B#zQI_ah;ZiD9ENI#j72kywbbH|m)}cY zmoEeIhmP9krYW!fC0#Q#B4kI6S8&SyHZH^pnDu@KC+-G=D=LYoO{Sf~}+`sxTeZlhU>*%J?d z9L$e_l9F{sv#=L~)8c@%G6x4O=PJSE=d84p6=fM1^-&%P%+IceoQhu90I)@0Is>)c z4dc_BfGJ;aXFKQC?jP_Q)q{TXF)gEZrr9Il<#Xss-p(er)doCWOoef32S7&YCyTAY z$KWlJ5;63V(ak8QXWo6@OP3}0-TO<-0(pVQxVQ&{(0Orr$uP@)FPQEBFcHZDPxacD z8xNGDp4gg}nY|oD51Fjx^qE<>1eRBo%c+6trLwXg#rn)KRG@^74EA*vX=&c-L{Rrhf+5L#@_w?{xuISt(Pru6+I1QeRB&+(2cYUG0Yj1 zCtp(T`Ha{CftbEyAUp2X1Jf`2iC>Te!|^z+_7u9FIF1v1U%Za6u+2*5%_KUd}KiKSGLs?%jl|h%w_Wmse>` zuIjR+Pk!V_P61SU87M4sI{LXjK<0E*n#9^ADOWG`&cybr>rp~Ty^`*fsD@uF5$D9e9VTI9^D7%e<>mPkI8ZR+JxuQua8#JNe*QO%wsbQ9%}d9d+8@!zAV3*X z{ubTo;qne-)A}p>F}cbPQ-pF^?^lca#YbS#>iy^m`pl>hZBDegN zX-1b<0C2zSQ4J<+jee;*EhW^RM~WbhO*@&%hqlzY>5jh7JyXZF1lKR+ri(ZEFAm(u2MV8_C-Gyer;=p1Mg&X;P3VOzf>sM$*7RDCOtokX*BEgCsRPv9l#2`Qo zNTTxdR*k^|`|sZX3f@`9B6k?1-JXb6AdG)MnZMNOL`NoaH$wdlPiyHP$hueJk<0a4naBuxbSWvZ6rnUi zn>;nvpk6xEIwU}Xv45v-NRzxC*m+I7CdDpAMlV6;B57IUxehTx%9+Hp^xDr9K7%`W z5CvSYD_V>c%`|S^5>`>Q?`G$r{gn4-PvCM+iequBdamiD^UJ4Gj3CT9J~8@gkJwF$ zCB~^RbP>xMPaX z5P;}J-aQ41)lD<5+I_~+5>QeO8B0fMaA}(ia>;8<6nuPjVT<(1ZRYvz(Uhy!G8UKy zQ$d+v;wU!| zGP~JwepYE=gZ{`^`bbB<^me4QZss-R;er@C_--pEAV^5OgGzA`Vf-g{CIA>l?QP*1 zIOc~^cZAx$_wU4<%#4nhN7I=5AzpO@L#rOK>JHk^Y4)+)tgb$S%j$=wO2E)ag)4eS zq_U~Tg7eG6xp&V_p~L{&>dOl+KZ=Psj`s5BO{JXAV=uKwVp*b+M(h%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 06/60] 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 07/60] 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 08/60] 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 09/60] 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 10/60] 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 11/60] [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 12/60] 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 13/60] 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 14/60] 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 15/60] 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 16/60] [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 17/60] 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 18/60] 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 19/60] 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 20/60] 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 21/60] 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 22/60] 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 23/60] 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 24/60] 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 25/60] 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 26/60] 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 27/60] 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 28/60] 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 29/60] 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 30/60] 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( From 5997fc3d6f51b69cc176ad45ee35fdc68a15b2b0 Mon Sep 17 00:00:00 2001 From: Mary Gouseti Date: Tue, 10 Dec 2024 10:12:30 +0200 Subject: [PATCH 31/60] Introduce data stream options via dedicated classes (#117357) In this PR we move the failure store configuration from the `data_stream` in a composable index template to the `template` section under the `data_stream_options`. The reason for this change is that we want to be able to compose the failure store configuration via component templates. **Previous composable index template** ``` { "index_patterns": [....], "template": { ...... }, "data_stream": { "failure_store": true } } ``` **New composable index template** ``` { "index_patterns": [....], "template": { "data_stream_options": { "failure_store": { "enabled": true } }, ...... }, "data_stream": {} } ``` **Composition logic** | Component Template A | Composable Index Template | Resolved data stream options | |-------------------------|-----------------------------|-------------------------------| | `{"failure_store": {"enabled": true}}` | - |`{"failure_store": {"enabled": true}}`| | `{"failure_store": {"enabled": true}}` | `{"failure_store": {"enabled": false}}` |`{"failure_store": {"enabled": false}}`| | - | `{"failure_store": {"enabled": true}}` |`{"failure_store": {"enabled": true}}`| | `{"failure_store": null}` | `{"failure_store": {"enabled": true}}` |`{"failure_store": {"enabled": true}}`| | `{"failure_store": {"enabled": true}}` | `{"failure_store": null}` |`{}`| More context and discussions can be found in the comments of https://github.com/elastic/elasticsearch/pull/113863. --- modules/data-streams/build.gradle | 28 +++ .../datastreams/DataStreamIT.java | 4 +- ...lureStoreMetricsWithIncrementalBulkIT.java | 7 +- .../IngestFailureStoreMetricsIT.java | 7 +- .../DataStreamLifecycleServiceIT.java | 4 +- .../datastreams/DataStreamOptionsIT.java | 59 ++++- .../datastreams/FailureStoreQueryParamIT.java | 6 +- .../test/data_stream/10_basic.yml | 43 +++- .../test/data_stream/150_tsdb.yml | 15 +- .../data_stream/170_modify_data_stream.yml | 15 +- .../190_failure_store_redirection.yml | 144 +++++------- .../200_rollover_failure_store.yml | 40 ++-- .../30_auto_create_data_stream.yml | 15 +- .../org/elasticsearch/TransportVersions.java | 1 + .../post/SimulateIndexTemplateResponse.java | 5 +- .../TransportSimulateIndexTemplateAction.java | 9 +- .../action/bulk/TransportBulkAction.java | 18 +- .../metadata/ComposableIndexTemplate.java | 42 ++-- .../cluster/metadata/DataStream.java | 3 +- .../metadata/DataStreamFailureStore.java | 91 +++++++- .../cluster/metadata/DataStreamOptions.java | 128 ++++++++++- .../MetadataCreateDataStreamService.java | 11 +- .../MetadataIndexTemplateService.java | 105 ++++++++- .../cluster/metadata/ResettableValue.java | 216 ++++++++++++++++++ .../cluster/metadata/Template.java | 70 +++++- .../RestPutComponentTemplateAction.java | 10 + .../RestPutComposableIndexTemplateAction.java | 10 + .../GetComponentTemplateResponseTests.java | 8 +- .../action/bulk/BulkOperationTests.java | 7 +- .../bulk/TransportBulkActionIngestTests.java | 4 +- .../action/bulk/TransportBulkActionTests.java | 39 +++- .../metadata/ComponentTemplateTests.java | 49 ++-- .../ComposableIndexTemplateTests.java | 6 +- .../DataStreamFailureStoreTemplateTests.java | 65 ++++++ .../metadata/DataStreamFailureStoreTests.java | 2 +- .../DataStreamOptionsTemplateTests.java | 116 ++++++++++ .../metadata/DataStreamTemplateTests.java | 11 +- .../cluster/metadata/DataStreamTests.java | 2 +- .../MetadataCreateDataStreamServiceTests.java | 18 +- .../MetadataIndexTemplateServiceTests.java | 141 +++++++++++- .../metadata/ResettableValueTests.java | 103 +++++++++ .../metadata/DataStreamTestHelper.java | 20 +- .../xpack/core/DataStreamRestIT.java | 14 +- .../rest-api-spec/test/20_failure_store.yml | 11 +- 44 files changed, 1458 insertions(+), 264 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/cluster/metadata/ResettableValue.java create mode 100644 server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamFailureStoreTemplateTests.java create mode 100644 server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamOptionsTemplateTests.java create mode 100644 server/src/test/java/org/elasticsearch/cluster/metadata/ResettableValueTests.java diff --git a/modules/data-streams/build.gradle b/modules/data-streams/build.gradle index b017ae9921b0..8ae56101ef01 100644 --- a/modules/data-streams/build.gradle +++ b/modules/data-streams/build.gradle @@ -42,4 +42,32 @@ if (buildParams.isSnapshotBuild() == false) { tasks.named("yamlRestCompatTestTransform").configure({ task -> task.skipTest("data_stream/10_basic/Create hidden data stream", "warning does not exist for compatibility") + + // Failure store configuration changed on 8.18 (earlier versions behind feature flag) + task.skipTest("data_stream/10_basic/Create data stream with failure store", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/10_basic/Delete data stream with failure store", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/10_basic/Delete data stream with failure store uninitialized", "Configuring the failure store via data stream templates is not supported anymore.") + + task.skipTest("data_stream/30_auto_create_data_stream/Don't initialize failure store during data stream auto-creation on successful index", "Configuring the failure store via data stream templates is not supported anymore.") + + task.skipTest("data_stream/150_tsdb/TSDB failures go to failure store", "Configuring the failure store via data stream templates is not supported anymore.") + + task.skipTest("data_stream/170_modify_data_stream/Modify a data stream's failure store", "Configuring the failure store via data stream templates is not supported anymore.") + + task.skipTest("data_stream/190_failure_store_redirection/Failure redirects to original failure store during index change if final pipeline changes target", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Ensure failure is redirected to correct failure store after a reroute processor", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Test failure store status with bulk request", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Redirect ingest failure in data stream to failure store", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Failure redirects to correct failure store when pipeline loop is detected", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Failure redirects to correct failure store when index loop is detected", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Failure redirects to original failure store during index change if self referenced", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Redirect shard failure in data stream to failure store", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/190_failure_store_redirection/Version conflicts are not redirected to failure store", "Configuring the failure store via data stream templates is not supported anymore.") + + task.skipTest("data_stream/200_rollover_failure_store/Lazily roll over a data stream's failure store after an ingest failure", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/200_rollover_failure_store/A failure store marked for lazy rollover should only be rolled over when there is a failure", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/200_rollover_failure_store/Roll over a data stream's failure store without conditions", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/200_rollover_failure_store/Lazily roll over a data stream's failure store after a shard failure", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/200_rollover_failure_store/Don't roll over a data stream's failure store when conditions aren't met", "Configuring the failure store via data stream templates is not supported anymore.") + task.skipTest("data_stream/200_rollover_failure_store/Roll over a data stream's failure store with conditions", "Configuring the failure store via data stream templates is not supported anymore.") }) diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java index 777ddc28fefd..0e03045a090f 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java @@ -67,6 +67,7 @@ import org.elasticsearch.cluster.metadata.DataStreamAction; import org.elasticsearch.cluster.metadata.DataStreamAlias; import org.elasticsearch.cluster.metadata.DataStreamLifecycle; +import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexMetadataStats; import org.elasticsearch.cluster.metadata.IndexWriteLoad; @@ -2447,9 +2448,10 @@ static void putComposableIndexTemplate( .mappings(mappings == null ? null : CompressedXContent.fromJSON(mappings)) .aliases(aliases) .lifecycle(lifecycle) + .dataStreamOptions(DataStreamTestHelper.createDataStreamOptionsTemplate(withFailureStore)) ) .metadata(metadata) - .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false, withFailureStore)) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .build() ); client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet(); diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/FailureStoreMetricsWithIncrementalBulkIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/FailureStoreMetricsWithIncrementalBulkIT.java index 2c9b7417b283..d27ec04179e1 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/FailureStoreMetricsWithIncrementalBulkIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/FailureStoreMetricsWithIncrementalBulkIT.java @@ -21,6 +21,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.DataStream; +import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; @@ -165,8 +166,8 @@ private void createDataStreamWithFailureStore() throws IOException { request.indexTemplate( ComposableIndexTemplate.builder() .indexPatterns(List.of(DATA_STREAM_NAME + "*")) - .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false, true)) - .template(new Template(null, new CompressedXContent(""" + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(Template.builder().mappings(new CompressedXContent(""" { "dynamic": false, "properties": { @@ -177,7 +178,7 @@ private void createDataStreamWithFailureStore() throws IOException { "type": "long" } } - }"""), null)) + }""")).dataStreamOptions(DataStreamTestHelper.createDataStreamOptionsTemplate(true))) .build() ); assertAcked(safeGet(client().execute(TransportPutComposableIndexTemplateAction.TYPE, request))); diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/IngestFailureStoreMetricsIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/IngestFailureStoreMetricsIT.java index 96def04069e2..e9eaf7b5fadd 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/IngestFailureStoreMetricsIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/IngestFailureStoreMetricsIT.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; +import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.compress.CompressedXContent; @@ -283,8 +284,8 @@ private void putComposableIndexTemplate(boolean failureStore) throws IOException request.indexTemplate( ComposableIndexTemplate.builder() .indexPatterns(List.of(dataStream + "*")) - .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false, failureStore)) - .template(new Template(null, new CompressedXContent(""" + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(Template.builder().mappings(new CompressedXContent(""" { "dynamic": false, "properties": { @@ -295,7 +296,7 @@ private void putComposableIndexTemplate(boolean failureStore) throws IOException "type": "long" } } - }"""), null)) + }""")).dataStreamOptions(DataStreamTestHelper.createDataStreamOptionsTemplate(failureStore))) .build() ); client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet(); diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceIT.java index 89c440f5edf8..19067d85a680 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleServiceIT.java @@ -40,6 +40,7 @@ import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.DataStreamAction; import org.elasticsearch.cluster.metadata.DataStreamLifecycle; +import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -1226,9 +1227,10 @@ static void putComposableIndexTemplate( .settings(settings) .mappings(mappings == null ? null : CompressedXContent.fromJSON(mappings)) .lifecycle(lifecycle) + .dataStreamOptions(DataStreamTestHelper.createDataStreamOptionsTemplate(withFailureStore)) ) .metadata(metadata) - .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false, withFailureStore)) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .build() ); client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet(); diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/DataStreamOptionsIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/DataStreamOptionsIT.java index 980cc32a12c6..de6b7a682324 100644 --- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/DataStreamOptionsIT.java +++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/DataStreamOptionsIT.java @@ -11,12 +11,14 @@ import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; import org.junit.Before; import java.io.IOException; import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -40,10 +42,14 @@ public void setup() throws IOException { "template": { "settings": { "number_of_replicas": 0 + }, + "data_stream_options": { + "failure_store": { + "enabled": true + } } }, "data_stream": { - "failure_store": true } } """); @@ -59,12 +65,63 @@ public void setup() throws IOException { assertThat(dataStreams.size(), is(1)); Map dataStream = (Map) dataStreams.get(0); assertThat(dataStream.get("name"), equalTo(DATA_STREAM_NAME)); + assertThat(((Map) dataStream.get("failure_store")).get("enabled"), is(true)); List backingIndices = getIndices(dataStream); assertThat(backingIndices.size(), is(1)); List failureStore = getFailureStore(dataStream); assertThat(failureStore.size(), is(1)); } + public void testExplicitlyResetDataStreamOptions() throws IOException { + Request putComponentTemplateRequest = new Request("POST", "/_component_template/with-options"); + putComponentTemplateRequest.setJsonEntity(""" + { + "template": { + "data_stream_options": { + "failure_store": { + "enabled": true + } + } + } + } + """); + assertOK(client().performRequest(putComponentTemplateRequest)); + + Request invalidRequest = new Request("POST", "/_index_template/other-template"); + invalidRequest.setJsonEntity(""" + { + "index_patterns": ["something-else"], + "composed_of" : ["with-options"], + "template": { + "settings": { + "number_of_replicas": 0 + } + } + } + """); + Exception error = expectThrows(ResponseException.class, () -> client().performRequest(invalidRequest)); + assertThat( + error.getMessage(), + containsString("specifies data stream options that can only be used in combination with a data stream") + ); + + // Check that when we nullify the data stream options we can create use any component template in a non data stream template + Request otherRequest = new Request("POST", "/_index_template/other-template"); + otherRequest.setJsonEntity(""" + { + "index_patterns": ["something-else"], + "composed_of" : ["with-options"], + "template": { + "settings": { + "number_of_replicas": 0 + }, + "data_stream_options": null + } + } + """); + assertOK(client().performRequest(otherRequest)); + } + public void testEnableDisableFailureStore() throws IOException { { assertAcknowledged(client().performRequest(new Request("DELETE", "/_data_stream/" + DATA_STREAM_NAME + "/_options"))); diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/FailureStoreQueryParamIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/FailureStoreQueryParamIT.java index 20ec26c0c934..85b914be30b2 100644 --- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/FailureStoreQueryParamIT.java +++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/FailureStoreQueryParamIT.java @@ -42,10 +42,14 @@ public void setup() throws IOException { "template": { "settings": { "number_of_replicas": 0 + }, + "data_stream_options": { + "failure_store": { + "enabled": true + } } }, "data_stream": { - "failure_store": true } } """); diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/10_basic.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/10_basic.yml index 044ea90fec1a..0d19f555d10a 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/10_basic.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/10_basic.yml @@ -212,8 +212,12 @@ setup: --- "Create data stream with failure store": - requires: - cluster_features: ["gte_v8.15.0"] - reason: "data stream failure stores default settings changed in 8.15+" + test_runner_features: [ capabilities, allowed_warnings ] + reason: "Data stream failure stores config in templates was added in 8.18+" + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] - do: ingest.put_pipeline: @@ -256,8 +260,7 @@ setup: name: my-template4 body: index_patterns: [ failure-data-stream1, failure-data-stream2 ] - data_stream: - failure_store: true + data_stream: {} template: settings: index: @@ -269,6 +272,9 @@ setup: type: date count: type: long + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: @@ -632,8 +638,12 @@ setup: --- "Delete data stream with failure store": - requires: - cluster_features: ["gte_v8.15.0"] - reason: "data stream failure stores REST structure changed in 8.15+" + reason: "Data stream failure stores config in templates was added in 8.18+" + test_runner_features: [ allowed_warnings, capabilities ] + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] - do: allowed_warnings: @@ -642,8 +652,7 @@ setup: name: my-template4 body: index_patterns: [ failure-data-stream1 ] - data_stream: - failure_store: true + data_stream: {} template: mappings: properties: @@ -651,6 +660,9 @@ setup: type: date count: type: long + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: @@ -722,8 +734,12 @@ setup: --- "Delete data stream with failure store uninitialized": - requires: - cluster_features: ["gte_v8.15.0"] - reason: "data stream failure stores REST structure changed in 8.15+" + reason: "Data stream failure stores config in templates was added in 8.18+" + test_runner_features: [ capabilities, allowed_warnings ] + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] - do: allowed_warnings: @@ -732,8 +748,11 @@ setup: name: my-template4 body: index_patterns: [ failure-data-stream1 ] - data_stream: - failure_store: true + data_stream: {} + template: + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml index 3fbf85ab1e70..9ea3bfefabdf 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml @@ -185,9 +185,12 @@ index without timestamp: --- TSDB failures go to failure store: - requires: - cluster_features: ["data_stream.failure_store.tsdb_fix"] - reason: "tests tsdb failure store fixes in 8.16.0 that catch timestamp errors that happen earlier in the process and redirect them to the failure store." - + reason: "Data stream failure stores config in templates was added in 8.18+" + test_runner_features: [ capabilities, allowed_warnings ] + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] - do: allowed_warnings: - "index template [my-template2] has index patterns [fs-k8s*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template2] will take precedence during new index creation" @@ -195,8 +198,7 @@ TSDB failures go to failure store: name: my-template2 body: index_patterns: [ "fs-k8s*" ] - data_stream: - failure_store: true + data_stream: {} template: settings: index: @@ -207,6 +209,9 @@ TSDB failures go to failure store: time_series: start_time: 2021-04-28T00:00:00Z end_time: 2021-04-29T00:00:00Z + data_stream_options: + failure_store: + enabled: true mappings: properties: "@timestamp": diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/170_modify_data_stream.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/170_modify_data_stream.yml index 3c6d29d93922..13f79e95d99f 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/170_modify_data_stream.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/170_modify_data_stream.yml @@ -92,9 +92,12 @@ --- "Modify a data stream's failure store": - requires: - cluster_features: ["gte_v8.15.0"] - reason: "data stream failure stores REST structure changed in 8.15+" - test_runner_features: [ "allowed_warnings" ] + reason: "Data stream failure stores config in templates was added in 8.18+" + test_runner_features: [ capabilities, allowed_warnings ] + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] - do: allowed_warnings: @@ -103,8 +106,7 @@ name: my-template body: index_patterns: [data-*] - data_stream: - failure_store: true + data_stream: {} template: mappings: properties: @@ -112,6 +114,9 @@ type: date count: type: long + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml index 9b5a9dae8bc0..2f6b7a0bff34 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml @@ -1,3 +1,15 @@ +setup: + - requires: + reason: "Data stream options was added in 8.18+" + test_runner_features: [ capabilities, allowed_warnings, contains ] + capabilities: + - method: POST + path: /{index}/_doc + capabilities: [ 'failure_store_status' ] + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] + --- teardown: - do: @@ -32,13 +44,6 @@ teardown: --- "Redirect ingest failure in data stream to failure store": - - requires: - reason: "Failure store status was added in 8.16+" - test_runner_features: [capabilities, allowed_warnings, contains] - capabilities: - - method: POST - path: /{index}/_doc - capabilities: [ 'failure_store_status' ] - do: ingest.put_pipeline: id: "failing_pipeline" @@ -78,14 +83,16 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: default_pipeline: "parent_failing_pipeline" + data_stream_options: + failure_store: + enabled: true - do: index: @@ -147,14 +154,6 @@ teardown: --- "Redirect shard failure in data stream to failure store": - - requires: - reason: "Failure store status was added in 8.16+" - test_runner_features: [ capabilities, allowed_warnings, contains ] - capabilities: - - method: POST - path: /{index}/_doc - capabilities: [ 'failure_store_status' ] - - do: allowed_warnings: - "index template [generic_logs_template] has index patterns [logs-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [generic_logs_template] will take precedence during new index creation" @@ -162,8 +161,7 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 @@ -174,7 +172,9 @@ teardown: type: date count: type: long - + data_stream_options: + failure_store: + enabled: true - do: index: @@ -231,13 +231,6 @@ teardown: --- "Ensure failure is redirected to correct failure store after a reroute processor": - - requires: - test_runner_features: [allowed_warnings, capabilities] - reason: "Failure store status was added in 8.16+" - capabilities: - - method: POST - path: /{index}/_doc - capabilities: [ 'failure_store_status' ] - do: ingest.put_pipeline: id: "failing_pipeline" @@ -262,14 +255,16 @@ teardown: name: destination_template body: index_patterns: destination-data-stream - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: default_pipeline: "failing_pipeline" + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: @@ -331,11 +326,6 @@ teardown: --- "Failure redirects to original failure store during index change if self referenced": - - requires: - cluster_features: [ "gte_v8.15.0" ] - reason: "data stream failure stores REST structure changed in 8.15+" - test_runner_features: [ allowed_warnings, contains ] - - do: ingest.put_pipeline: id: "failing_pipeline" @@ -365,14 +355,16 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: default_pipeline: "failing_pipeline" + data_stream_options: + failure_store: + enabled: true - do: index: @@ -430,14 +422,6 @@ teardown: --- "Failure redirects to original failure store during index change if final pipeline changes target": - - requires: - reason: "Failure store status was added in 8.16+" - test_runner_features: [ capabilities, allowed_warnings, contains ] - capabilities: - - method: POST - path: /{index}/_doc - capabilities: [ 'failure_store_status' ] - - do: ingest.put_pipeline: id: "change_index_pipeline" @@ -462,14 +446,16 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: final_pipeline: "change_index_pipeline" + data_stream_options: + failure_store: + enabled: true - do: index: @@ -526,14 +512,6 @@ teardown: --- "Failure redirects to correct failure store when index loop is detected": - - requires: - reason: "Failure store status was added in 8.16+" - test_runner_features: [ capabilities, allowed_warnings, contains ] - capabilities: - - method: POST - path: /{index}/_doc - capabilities: [ 'failure_store_status' ] - - do: ingest.put_pipeline: id: "send_to_destination" @@ -575,14 +553,16 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: default_pipeline: "send_to_destination" + data_stream_options: + failure_store: + enabled: true - do: allowed_warnings: @@ -591,14 +571,16 @@ teardown: name: destination_logs_template body: index_patterns: destination-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: default_pipeline: "send_back_to_original" + data_stream_options: + failure_store: + enabled: true - do: index: @@ -657,14 +639,6 @@ teardown: --- "Failure redirects to correct failure store when pipeline loop is detected": - - requires: - reason: "Failure store status was added in 8.16+" - test_runner_features: [ capabilities, allowed_warnings, contains ] - capabilities: - - method: POST - path: /{index}/_doc - capabilities: [ 'failure_store_status' ] - - do: ingest.put_pipeline: id: "step_1" @@ -706,14 +680,16 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 index: default_pipeline: "step_1" + data_stream_options: + failure_store: + enabled: true - do: index: @@ -773,9 +749,6 @@ teardown: --- "Version conflicts are not redirected to failure store": - - requires: - test_runner_features: [ allowed_warnings] - - do: allowed_warnings: - "index template [generic_logs_template] has index patterns [logs-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [generic_logs_template] will take precedence during new index creation" @@ -783,8 +756,7 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 @@ -795,6 +767,9 @@ teardown: type: date count: type: long + data_stream_options: + failure_store: + enabled: true - do: bulk: @@ -812,17 +787,6 @@ teardown: --- "Test failure store status with bulk request": - - requires: - test_runner_features: [ allowed_warnings, capabilities ] - reason: "Failure store status was added in 8.16+" - capabilities: - - method: POST - path: /_bulk - capabilities: [ 'failure_store_status' ] - - method: PUT - path: /_bulk - capabilities: [ 'failure_store_status' ] - - do: allowed_warnings: - "index template [generic_logs_template] has index patterns [logs-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [generic_logs_template] will take precedence during new index creation" @@ -830,8 +794,7 @@ teardown: name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 @@ -842,6 +805,9 @@ teardown: type: date count: type: long + data_stream_options: + failure_store: + enabled: true - do: allowed_warnings: - "index template [no-fs] has index patterns [no-fs*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [no-fs] will take precedence during new index creation" @@ -849,8 +815,7 @@ teardown: name: no-fs body: index_patterns: no-fs* - data_stream: - failure_store: false + data_stream: {} template: settings: number_of_shards: 1 @@ -861,6 +826,9 @@ teardown: type: date count: type: long + data_stream_options: + failure_store: + enabled: false - do: diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml index 0742435f045f..cc3a11ffde5e 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml @@ -1,9 +1,15 @@ --- setup: - requires: - cluster_features: ["gte_v8.15.0"] - reason: "data stream failure stores REST structure changed in 8.15+" - test_runner_features: [allowed_warnings, contains, capabilities] + reason: "Data stream failure stores config in templates was added in 8.16+" + test_runner_features: [ capabilities, allowed_warnings ] + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] + - method: POST + path: /{index}/_rollover + capabilities: [ 'lazy-rollover-failure-store' ] - do: allowed_warnings: @@ -12,8 +18,7 @@ setup: name: my-template body: index_patterns: [data-*] - data_stream: - failure_store: true + data_stream: {} template: mappings: properties: @@ -21,6 +26,9 @@ setup: type: date count: type: long + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: @@ -145,14 +153,6 @@ teardown: --- "Lazily roll over a data stream's failure store after a shard failure": - - requires: - reason: "data stream failure store lazy rollover only supported in 8.15+" - test_runner_features: [allowed_warnings, capabilities] - capabilities: - - method: POST - path: /{index}/_rollover - capabilities: [lazy-rollover-failure-store] - # Initialize failure store - do: index: @@ -215,14 +215,6 @@ teardown: --- "Lazily roll over a data stream's failure store after an ingest failure": - - requires: - reason: "data stream failure store lazy rollover only supported in 8.15+" - test_runner_features: [allowed_warnings, capabilities] - capabilities: - - method: POST - path: /{index}/_rollover - capabilities: [lazy-rollover-failure-store] - - do: ingest.put_pipeline: id: "failing_pipeline" @@ -246,12 +238,14 @@ teardown: name: my-template body: index_patterns: [data-*] - data_stream: - failure_store: true + data_stream: {} template: settings: index: default_pipeline: "failing_pipeline" + data_stream_options: + failure_store: + enabled: true - do: indices.create_data_stream: diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/30_auto_create_data_stream.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/30_auto_create_data_stream.yml index 61d17c3d675c..60500767213a 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/30_auto_create_data_stream.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/30_auto_create_data_stream.yml @@ -50,9 +50,12 @@ --- "Don't initialize failure store during data stream auto-creation on successful index": - requires: - cluster_features: ["gte_v8.15.0"] - reason: "data stream failure stores REST structure changed in 8.15+" - test_runner_features: allowed_warnings + reason: "Data stream failure stores config in templates was added in 8.18+" + test_runner_features: [allowed_warnings, capabilities] + capabilities: + - method: POST + path: /_index_template/{template} + capabilities: [ 'failure_store_in_template' ] - do: allowed_warnings: @@ -61,12 +64,14 @@ name: generic_logs_template body: index_patterns: logs-* - data_stream: - failure_store: true + data_stream: {} template: settings: number_of_shards: 1 number_of_replicas: 1 + data_stream_options: + failure_store: + enabled: true - do: index: diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 40a209c5f0f1..36a8e3c40862 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -140,6 +140,7 @@ static TransportVersion def(int id) { public static final TransportVersion SOURCE_MODE_TELEMETRY = def(8_802_00_0); public static final TransportVersion NEW_REFRESH_CLUSTER_BLOCK = def(8_803_00_0); public static final TransportVersion RETRIES_AND_OPERATIONS_IN_BLOBSTORE_STATS = def(8_804_00_0); + public static final TransportVersion ADD_DATA_STREAM_OPTIONS_TO_TEMPLATES = def(8_805_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/SimulateIndexTemplateResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/SimulateIndexTemplateResponse.java index 80e6fbfe051a..a521dac60e96 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/SimulateIndexTemplateResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/SimulateIndexTemplateResponse.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.admin.indices.rollover.RolloverConfiguration; import org.elasticsearch.cluster.metadata.DataStreamGlobalRetention; +import org.elasticsearch.cluster.metadata.ResettableValue; import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -111,9 +112,9 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - if (this.resolvedTemplate != null) { + if (resolvedTemplate != null) { builder.field(TEMPLATE.getPreferredName()); - this.resolvedTemplate.toXContent(builder, params, rolloverConfiguration); + resolvedTemplate.toXContent(builder, ResettableValue.hideResetValues(params), rolloverConfiguration); } if (this.overlappingTemplates != null) { builder.startArray(OVERLAPPING.getPreferredName()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java index 94d9b87467ea..5f98852148ed 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/post/TransportSimulateIndexTemplateAction.java @@ -61,6 +61,7 @@ import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findConflictingV1Templates; import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findConflictingV2Templates; import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.findV2Template; +import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.resolveDataStreamOptions; import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.resolveLifecycle; import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.resolveSettings; @@ -348,7 +349,13 @@ public static Template resolveTemplate( if (template.getDataStreamTemplate() != null && lifecycle == null && isDslOnlyMode) { lifecycle = DataStreamLifecycle.DEFAULT; } - return new Template(settings, mergedMapping, aliasesByName, lifecycle); + return new Template( + settings, + mergedMapping, + aliasesByName, + lifecycle, + resolveDataStreamOptions(simulatedState.metadata(), matchingTemplate) + ); } private static IndexLongFieldRange getEventIngestedRange(String indexName, ClusterState simulatedState) { diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java index cef68324e2a4..e2c73349b93e 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java @@ -35,6 +35,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.DataStream; +import org.elasticsearch.cluster.metadata.DataStreamOptions; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.Metadata; @@ -42,6 +43,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.concurrent.AtomicArray; +import org.elasticsearch.core.Nullable; import org.elasticsearch.features.FeatureService; import org.elasticsearch.index.IndexingPressure; import org.elasticsearch.index.VersionType; @@ -638,12 +640,16 @@ private static Boolean resolveFailureStoreFromMetadata(String indexName, Metadat } /** - * Determines if an index name is associated with an index template that has a data stream failure store enabled. + * Determines if an index name is associated with an index template that has a data stream failure store enabled. Since failure store is + * a data stream feature, the method returns true/false only if it is a data stream template, otherwise null. * @param indexName The index name to check. * @param metadata Cluster state metadata. - * @return true if the given index name corresponds to an index template with a data stream failure store enabled. + * @return true the associated index template has failure store enabled, false if the failure store is disabled or it's not specified, + * and null if the template is not a data stream template. + * Visible for testing */ - private static Boolean resolveFailureStoreFromTemplate(String indexName, Metadata metadata) { + @Nullable + static Boolean resolveFailureStoreFromTemplate(String indexName, Metadata metadata) { if (indexName == null) { return null; } @@ -656,7 +662,11 @@ private static Boolean resolveFailureStoreFromTemplate(String indexName, Metadat ComposableIndexTemplate composableIndexTemplate = metadata.templatesV2().get(template); if (composableIndexTemplate.getDataStreamTemplate() != null) { // Check if the data stream has the failure store enabled - return composableIndexTemplate.getDataStreamTemplate().hasFailureStore(); + DataStreamOptions dataStreamOptions = MetadataIndexTemplateService.resolveDataStreamOptions( + composableIndexTemplate, + metadata.componentTemplates() + ).mapAndGet(DataStreamOptions.Template::toDataStreamOptions); + return dataStreamOptions != null && dataStreamOptions.isFailureStoreEnabled(); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java index ae7cff631215..998217e93c42 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.index.mapper.DataStreamTimestampFieldMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -372,16 +373,14 @@ public static class DataStreamTemplate implements Writeable, ToXContentObject { private static final ParseField HIDDEN = new ParseField("hidden"); private static final ParseField ALLOW_CUSTOM_ROUTING = new ParseField("allow_custom_routing"); + // Remove this after this PR gets backported + @UpdateForV9(owner = UpdateForV9.Owner.DATA_MANAGEMENT) private static final ParseField FAILURE_STORE = new ParseField("failure_store"); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "data_stream_template", false, - args -> new DataStreamTemplate( - args[0] != null && (boolean) args[0], - args[1] != null && (boolean) args[1], - DataStream.isFailureStoreFeatureFlagEnabled() && args[2] != null && (boolean) args[2] - ) + args -> new DataStreamTemplate(args[0] != null && (boolean) args[0], args[1] != null && (boolean) args[1]) ); static { @@ -394,20 +393,14 @@ public static class DataStreamTemplate implements Writeable, ToXContentObject { private final boolean hidden; private final boolean allowCustomRouting; - private final boolean failureStore; public DataStreamTemplate() { - this(false, false, false); + this(false, false); } public DataStreamTemplate(boolean hidden, boolean allowCustomRouting) { - this(hidden, allowCustomRouting, false); - } - - public DataStreamTemplate(boolean hidden, boolean allowCustomRouting, boolean failureStore) { this.hidden = hidden; this.allowCustomRouting = allowCustomRouting; - this.failureStore = failureStore; } DataStreamTemplate(StreamInput in) throws IOException { @@ -425,10 +418,9 @@ public DataStreamTemplate(boolean hidden, boolean allowCustomRouting, boolean fa boolean value = in.readBoolean(); assert value == false : "expected false, because this used to be an optional enum that never got set"; } - if (in.getTransportVersion().onOrAfter(DataStream.ADDED_FAILURE_STORE_TRANSPORT_VERSION)) { - failureStore = in.readBoolean(); - } else { - failureStore = false; + if (in.getTransportVersion() + .between(DataStream.ADDED_FAILURE_STORE_TRANSPORT_VERSION, TransportVersions.ADD_DATA_STREAM_OPTIONS_TO_TEMPLATES)) { + in.readBoolean(); } } @@ -458,10 +450,6 @@ public boolean isAllowCustomRouting() { return allowCustomRouting; } - public boolean hasFailureStore() { - return failureStore; - } - @Override public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(hidden); @@ -472,8 +460,11 @@ public void writeTo(StreamOutput out) throws IOException { // See comment in constructor. out.writeBoolean(false); } - if (out.getTransportVersion().onOrAfter(DataStream.ADDED_FAILURE_STORE_TRANSPORT_VERSION)) { - out.writeBoolean(failureStore); + if (out.getTransportVersion() + .between(DataStream.ADDED_FAILURE_STORE_TRANSPORT_VERSION, TransportVersions.ADD_DATA_STREAM_OPTIONS_TO_TEMPLATES)) { + // Previous versions expect the failure store to be configured via the DataStreamTemplate. We add it here, so we don't break + // the serialisation, but we do not care to preserve the value because this feature is still behind a feature flag. + out.writeBoolean(false); } } @@ -482,9 +473,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); builder.field("hidden", hidden); builder.field(ALLOW_CUSTOM_ROUTING.getPreferredName(), allowCustomRouting); - if (DataStream.isFailureStoreFeatureFlagEnabled()) { - builder.field(FAILURE_STORE.getPreferredName(), failureStore); - } builder.endObject(); return builder; } @@ -494,12 +482,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DataStreamTemplate that = (DataStreamTemplate) o; - return hidden == that.hidden && allowCustomRouting == that.allowCustomRouting && failureStore == that.failureStore; + return hidden == that.hidden && allowCustomRouting == that.allowCustomRouting; } @Override public int hashCode() { - return Objects.hash(hidden, allowCustomRouting, failureStore); + return Objects.hash(hidden, allowCustomRouting); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java index 979434950cf7..1c6206a4815e 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java @@ -424,7 +424,7 @@ public boolean isAllowCustomRouting() { * @return true, if the user has explicitly enabled the failure store. */ public boolean isFailureStoreEnabled() { - return dataStreamOptions.failureStore() != null && dataStreamOptions.failureStore().isExplicitlyEnabled(); + return dataStreamOptions.isFailureStoreEnabled(); } @Nullable @@ -1188,6 +1188,7 @@ public void writeTo(StreamOutput out) throws IOException { ); // The fields behind the feature flag should always be last. if (DataStream.isFailureStoreFeatureFlagEnabled()) { + // Should be removed after backport PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), FAILURE_STORE_FIELD); PARSER.declareObjectArray( ConstructingObjectParser.optionalConstructorArg(), diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java index e9d32594fa83..5a6217eea8f7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java @@ -14,7 +14,9 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; @@ -27,11 +29,13 @@ * supports the following configurations only explicitly enabling or disabling the failure store */ public record DataStreamFailureStore(Boolean enabled) implements SimpleDiffable, ToXContentObject { + public static final String FAILURE_STORE = "failure_store"; + public static final String ENABLED = "enabled"; - public static final ParseField ENABLED_FIELD = new ParseField("enabled"); + public static final ParseField ENABLED_FIELD = new ParseField(ENABLED); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "failure_store", + FAILURE_STORE, false, (args, unused) -> new DataStreamFailureStore((Boolean) args[0]) ); @@ -59,13 +63,6 @@ public static Diff readDiffFrom(StreamInput in) throws I return SimpleDiffable.readDiffFrom(DataStreamFailureStore::new, in); } - /** - * @return iff the user has explicitly enabled the failure store - */ - public boolean isExplicitlyEnabled() { - return enabled != null && enabled; - } - @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalBoolean(enabled); @@ -89,4 +86,80 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static DataStreamFailureStore fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + + /** + * This class is only used in template configuration. It wraps the fields of {@link DataStreamFailureStore} with {@link ResettableValue} + * to allow a user to signal when they want to reset any previously encountered values during template composition. Furthermore, it + * provides the method {@link #merge(Template, Template)} that dictates how two templates can be composed. + */ + public record Template(ResettableValue enabled) implements Writeable, ToXContentObject { + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "failure_store_template", + false, + (args, unused) -> new Template(args[0] == null ? ResettableValue.undefined() : (ResettableValue) args[0]) + ); + + static { + PARSER.declareField( + ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> p.currentToken() == XContentParser.Token.VALUE_NULL + ? ResettableValue.reset() + : ResettableValue.create(p.booleanValue()), + ENABLED_FIELD, + ObjectParser.ValueType.BOOLEAN_OR_NULL + ); + } + + public Template { + if (enabled.get() == null) { + throw new IllegalArgumentException("Failure store configuration should have at least one non-null configuration value."); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + ResettableValue.write(out, enabled, StreamOutput::writeBoolean); + } + + public static Template read(StreamInput in) throws IOException { + ResettableValue enabled = ResettableValue.read(in, StreamInput::readBoolean); + return new Template(enabled); + } + + /** + * Converts the template to XContent, depending on the XContent.Params set by {@link ResettableValue#hideResetValues(Params)} + * it may or may not display any explicit nulls when the value is to be reset. + */ + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + enabled.toXContent(builder, params, ENABLED_FIELD.getPreferredName()); + builder.endObject(); + return builder; + } + + public static Template fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + /** + * Returns a template which has the value of the initial template updated with the values of the update. + * Note: for now it's a trivial composition because we have only one non-null field. + * @return the composed template + */ + public static Template merge(Template ignored, Template update) { + return update; + } + + public DataStreamFailureStore toFailureStore() { + return new DataStreamFailureStore(enabled.get()); + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java index 9cd4e2625e2b..51e13c05e689 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java @@ -14,9 +14,9 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; @@ -24,6 +24,8 @@ import java.io.IOException; +import static org.elasticsearch.cluster.metadata.DataStreamFailureStore.FAILURE_STORE; + /** * Holds data stream dedicated configuration options such as failure store, (in the future lifecycle). Currently, it * supports the following configurations: @@ -34,10 +36,10 @@ public record DataStreamOptions(@Nullable DataStreamFailureStore failureStore) SimpleDiffable, ToXContentObject { - public static final ParseField FAILURE_STORE_FIELD = new ParseField("failure_store"); + public static final ParseField FAILURE_STORE_FIELD = new ParseField(FAILURE_STORE); public static final DataStreamOptions FAILURE_STORE_ENABLED = new DataStreamOptions(new DataStreamFailureStore(true)); public static final DataStreamOptions FAILURE_STORE_DISABLED = new DataStreamOptions(new DataStreamFailureStore(false)); - public static final DataStreamOptions EMPTY = new DataStreamOptions(); + public static final DataStreamOptions EMPTY = new DataStreamOptions(null); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "options", @@ -46,18 +48,13 @@ public record DataStreamOptions(@Nullable DataStreamFailureStore failureStore) ); static { - PARSER.declareField( + PARSER.declareObject( ConstructingObjectParser.optionalConstructorArg(), (p, c) -> DataStreamFailureStore.fromXContent(p), - FAILURE_STORE_FIELD, - ObjectParser.ValueType.OBJECT_OR_NULL + FAILURE_STORE_FIELD ); } - public DataStreamOptions() { - this(null); - } - public static DataStreamOptions read(StreamInput in) throws IOException { return new DataStreamOptions(in.readOptionalWriteable(DataStreamFailureStore::new)); } @@ -66,8 +63,21 @@ public static Diff readDiffFrom(StreamInput in) throws IOExce return SimpleDiffable.readDiffFrom(DataStreamOptions::read, in); } + /** + * @return true if none of the options are defined + */ public boolean isEmpty() { - return this.equals(EMPTY); + return failureStore == null; + } + + /** + * Determines if this data stream has its failure store enabled or not. Currently, the failure store + * is enabled only when a user has explicitly requested it. + * + * @return true, if the user has explicitly enabled the failure store. + */ + public boolean isFailureStoreEnabled() { + return failureStore != null && Boolean.TRUE.equals(failureStore.enabled()); } @Override @@ -93,4 +103,100 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static DataStreamOptions fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + + /** + * This class is only used in template configuration. It wraps the fields of {@link DataStreamOptions} with {@link ResettableValue} + * to allow a user to signal when they want to reset any previously encountered values during template composition. Furthermore, it + * provides the {@link Template.Builder} that dictates how two templates can be composed. + */ + public record Template(ResettableValue failureStore) implements Writeable, ToXContentObject { + public static final Template EMPTY = new Template(ResettableValue.undefined()); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "data_stream_options_template", + false, + (args, unused) -> new Template( + args[0] == null ? ResettableValue.undefined() : (ResettableValue) args[0] + ) + ); + + static { + PARSER.declareObjectOrNull( + ConstructingObjectParser.optionalConstructorArg(), + (p, s) -> ResettableValue.create(DataStreamFailureStore.Template.fromXContent(p)), + ResettableValue.reset(), + FAILURE_STORE_FIELD + ); + } + + public Template { + assert failureStore != null : "Template does not accept null values, please use Resettable.undefined()"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + ResettableValue.write(out, failureStore, (o, v) -> v.writeTo(o)); + } + + public static Template read(StreamInput in) throws IOException { + ResettableValue failureStore = ResettableValue.read(in, DataStreamFailureStore.Template::read); + return new Template(failureStore); + } + + public static Template fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + /** + * Converts the template to XContent, depending on the {@param params} set by {@link ResettableValue#hideResetValues(Params)} + * it may or may not display any explicit nulls when the value is to be reset. + */ + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + failureStore.toXContent(builder, params, FAILURE_STORE_FIELD.getPreferredName()); + builder.endObject(); + return builder; + } + + public DataStreamOptions toDataStreamOptions() { + return new DataStreamOptions(failureStore.mapAndGet(DataStreamFailureStore.Template::toFailureStore)); + } + + public static Builder builder(Template template) { + return new Builder(template); + } + + /** + * Builds and composes a data stream template. + */ + public static class Builder { + private ResettableValue failureStore = ResettableValue.undefined(); + + public Builder(Template template) { + if (template != null) { + failureStore = template.failureStore(); + } + } + + /** + * Updates the current failure store configuration with the provided value. This is not a replacement necessarily, if both + * instance contain data the configurations are merged. + */ + public Builder updateFailureStore(ResettableValue newFailureStore) { + failureStore = ResettableValue.merge(failureStore, newFailureStore, DataStreamFailureStore.Template::merge); + return this; + } + + public Template build() { + return new Template(failureStore); + } + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java index 5dbf4da6f376..0de87c722638 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java @@ -261,11 +261,16 @@ static ClusterState createDataStream( // This is not a problem as both have different prefixes (`.ds-` vs `.fs-`) and both will be using the same `generation` field // when rolling over in the future. final long initialGeneration = 1; + ResettableValue dataStreamOptionsTemplate = isSystem + ? MetadataIndexTemplateService.resolveDataStreamOptions(template, systemDataStreamDescriptor.getComponentTemplates()) + : MetadataIndexTemplateService.resolveDataStreamOptions(template, metadata.componentTemplates()); + final DataStreamOptions dataStreamOptions = dataStreamOptionsTemplate.mapAndGet(DataStreamOptions.Template::toDataStreamOptions); + var isFailureStoreEnabled = dataStreamOptions != null && dataStreamOptions.isFailureStoreEnabled(); // If we need to create a failure store, do so first. Do not reroute during the creation since we will do // that as part of creating the backing index if required. IndexMetadata failureStoreIndex = null; - if (template.getDataStreamTemplate().hasFailureStore() && initializeFailureStore) { + if (isFailureStoreEnabled && initializeFailureStore) { if (isSystem) { throw new IllegalArgumentException("Failure stores are not supported on system data streams"); } @@ -303,7 +308,7 @@ static ClusterState createDataStream( } assert writeIndex != null; assert writeIndex.mapping() != null : "no mapping found for backing index [" + writeIndex.getIndex().getName() + "]"; - assert template.getDataStreamTemplate().hasFailureStore() == false || initializeFailureStore == false || failureStoreIndex != null + assert isFailureStoreEnabled == false || initializeFailureStore == false || failureStoreIndex != null : "failure store should have an initial index"; assert failureStoreIndex == null || failureStoreIndex.mapping() != null : "no mapping found for failure store [" + failureStoreIndex.getIndex().getName() + "]"; @@ -329,7 +334,7 @@ static ClusterState createDataStream( template.getDataStreamTemplate().isAllowCustomRouting(), indexMode, lifecycle == null && isDslOnlyMode ? DataStreamLifecycle.DEFAULT : lifecycle, - template.getDataStreamTemplate().hasFailureStore() ? DataStreamOptions.FAILURE_STORE_ENABLED : DataStreamOptions.EMPTY, + dataStreamOptions, new DataStream.DataStreamIndices(DataStream.BACKING_INDEX_PREFIX, dsBackingIndices, false, null), // If the failure store shouldn't be initialized on data stream creation, we're marking it for "lazy rollover", which will // initialize the failure store on first write. diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java index 3878a3329b63..7f8b87d2d3f4 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java @@ -312,12 +312,7 @@ public ClusterState addComponentTemplate( } } - final Template finalTemplate = new Template( - finalSettings, - wrappedMappings, - template.template().aliases(), - template.template().lifecycle() - ); + final Template finalTemplate = Template.builder(template.template()).settings(finalSettings).mappings(wrappedMappings).build(); final ComponentTemplate finalComponentTemplate = new ComponentTemplate( finalTemplate, template.version(), @@ -348,6 +343,7 @@ public ClusterState addComponentTemplate( composableTemplate, globalRetentionSettings.get() ); + validateDataStreamOptions(tempStateWithComponentTemplateAdded.metadata(), composableTemplateName, composableTemplate); validateIndexTemplateV2(composableTemplateName, composableTemplate, tempStateWithComponentTemplateAdded); } catch (Exception e) { if (validationFailure == null) { @@ -629,7 +625,7 @@ public ClusterState addIndexTemplateV2( // adjusted (to add _doc) and it should be validated CompressedXContent mappings = innerTemplate.mappings(); CompressedXContent wrappedMappings = wrapMappingsIfNecessary(mappings, xContentRegistry); - final Template finalTemplate = new Template(finalSettings, wrappedMappings, innerTemplate.aliases(), innerTemplate.lifecycle()); + final Template finalTemplate = Template.builder(innerTemplate).settings(finalSettings).mappings(wrappedMappings).build(); finalIndexTemplate = template.toBuilder().template(finalTemplate).build(); } @@ -690,7 +686,8 @@ public static Map> v2TemplateOverlaps( return overlaps; } - private void validateIndexTemplateV2(String name, ComposableIndexTemplate indexTemplate, ClusterState currentState) { + // Visibility for testing + void validateIndexTemplateV2(String name, ComposableIndexTemplate indexTemplate, ClusterState currentState) { // Workaround for the fact that start_time and end_time are injected by the MetadataCreateDataStreamService upon creation, // but when validating templates that create data streams the MetadataCreateDataStreamService isn't used. var finalTemplate = indexTemplate.template(); @@ -726,6 +723,7 @@ private void validateIndexTemplateV2(String name, ComposableIndexTemplate indexT validate(name, templateToValidate); validateDataStreamsStillReferenced(currentState, name, templateToValidate); validateLifecycle(currentState.metadata(), name, templateToValidate, globalRetentionSettings.get()); + validateDataStreamOptions(currentState.metadata(), name, templateToValidate); if (templateToValidate.isDeprecated() == false) { validateUseOfDeprecatedComponentTemplates(name, templateToValidate, currentState.metadata().componentTemplates()); @@ -819,6 +817,20 @@ static void validateLifecycle( } } + // Visible for testing + static void validateDataStreamOptions(Metadata metadata, String indexTemplateName, ComposableIndexTemplate template) { + ResettableValue dataStreamOptions = resolveDataStreamOptions(template, metadata.componentTemplates()); + if (dataStreamOptions.get() != null) { + if (template.getDataStreamTemplate() == null) { + throw new IllegalArgumentException( + "index template [" + + indexTemplateName + + "] specifies data stream options that can only be used in combination with a data stream" + ); + } + } + } + /** * Validate that by changing or adding {@code newTemplate}, there are * no unreferenced data streams. Note that this scenario is still possible @@ -1561,7 +1573,7 @@ static List> resolveAliases( public static DataStreamLifecycle resolveLifecycle(final Metadata metadata, final String templateName) { final ComposableIndexTemplate template = metadata.templatesV2().get(templateName); assert template != null - : "attempted to resolve settings for a template [" + templateName + "] that did not exist in the cluster state"; + : "attempted to resolve lifecycle for a template [" + templateName + "] that did not exist in the cluster state"; if (template == null) { return null; } @@ -1653,6 +1665,81 @@ public static DataStreamLifecycle composeDataLifecycles(List} object + */ + public static ResettableValue resolveDataStreamOptions(final Metadata metadata, final String templateName) { + final ComposableIndexTemplate template = metadata.templatesV2().get(templateName); + assert template != null + : "attempted to resolve data stream options for a template [" + templateName + "] that did not exist in the cluster state"; + if (template == null) { + return ResettableValue.undefined(); + } + return resolveDataStreamOptions(template, metadata.componentTemplates()); + } + + /** + * Resolve the provided v2 template and component templates into a {@link ResettableValue} object + */ + public static ResettableValue resolveDataStreamOptions( + ComposableIndexTemplate template, + Map componentTemplates + ) { + Objects.requireNonNull(template, "attempted to resolve data stream for a null template"); + Objects.requireNonNull(componentTemplates, "attempted to resolve data stream options with null component templates"); + + List> dataStreamOptionsList = new ArrayList<>(); + for (String componentTemplateName : template.composedOf()) { + if (componentTemplates.containsKey(componentTemplateName) == false) { + continue; + } + ResettableValue dataStreamOptions = componentTemplates.get(componentTemplateName) + .template() + .resettableDataStreamOptions(); + if (dataStreamOptions.isDefined()) { + dataStreamOptionsList.add(dataStreamOptions); + } + } + // The actual index template's data stream options have the highest precedence. + if (template.template() != null && template.template().resettableDataStreamOptions().isDefined()) { + dataStreamOptionsList.add(template.template().resettableDataStreamOptions()); + } + return composeDataStreamOptions(dataStreamOptionsList); + } + + /** + * This method composes a series of data streams options to a final one. Since currently the data stream options + * contains only the failure store configuration which also contains only one field, the composition is a bit trivial. + * But we introduce the mechanics that will help extend it really easily. + * @param dataStreamOptionsList a sorted list of data stream options in the order that they will be composed + * @return the final data stream option configuration + */ + public static ResettableValue composeDataStreamOptions( + List> dataStreamOptionsList + ) { + if (dataStreamOptionsList.isEmpty()) { + return ResettableValue.undefined(); + } + DataStreamOptions.Template.Builder builder = null; + for (ResettableValue current : dataStreamOptionsList) { + if (current.isDefined() == false) { + continue; + } + if (current.shouldReset()) { + builder = null; + } else { + DataStreamOptions.Template currentTemplate = current.get(); + if (builder == null) { + builder = DataStreamOptions.Template.builder(currentTemplate); + } else { + // Currently failure store has only one field that needs to be defined so the composing of the failure store is trivial + builder.updateFailureStore(currentTemplate.failureStore()); + } + } + } + return builder == null ? ResettableValue.undefined() : ResettableValue.create(builder.build()); + } + /** * Given a state and a composable template, validate that the final composite template * generated by the composable template and all of its component templates contains valid diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ResettableValue.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ResettableValue.java new file mode 100644 index 000000000000..4f38d2b8386a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ResettableValue.java @@ -0,0 +1,216 @@ +/* + * 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.cluster.metadata; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * This class holds a value of type @{param T} that can be in one of 3 states: + * - It has a concrete value, or + * - It is missing, or + * - It is meant to reset any other when it is composed with it. + * It is mainly used in template composition to capture the case when the user wished to reset any previous values. + * @param + */ +public class ResettableValue { + private static final ResettableValue RESET = new ResettableValue<>(true, null); + private static final ResettableValue UNDEFINED = new ResettableValue<>(false, null); + private static final String DISPLAY_RESET_VALUES = "display_reset"; + private static final Map HIDE_RESET_VALUES_PARAMS = Map.of(DISPLAY_RESET_VALUES, "false"); + + private final T value; + private final boolean isDefined; + + /** + * @return the reset state, meaning that this value is explicitly requested to be reset + */ + public static ResettableValue reset() { + @SuppressWarnings("unchecked") + ResettableValue t = (ResettableValue) RESET; + return t; + } + + /** + * @return the undefined state, meaning that this value has not been specified + */ + public static ResettableValue undefined() { + @SuppressWarnings("unchecked") + ResettableValue t = (ResettableValue) UNDEFINED; + return t; + } + + /** + * Wraps a value, if the value is null, it returns {@link #undefined()} + */ + public static ResettableValue create(T value) { + if (value == null) { + return undefined(); + } + return new ResettableValue<>(true, value); + } + + private ResettableValue(boolean isDefined, T value) { + this.isDefined = isDefined; + this.value = value; + } + + /** + * @return true if the state of this is reset + */ + public boolean shouldReset() { + return isDefined && value == null; + } + + /** + * @return true when the value is defined, either with a concrete value or reset. + */ + public boolean isDefined() { + return isDefined; + } + + /** + * @return the concrete value or null if it is in undefined or reset states. + */ + @Nullable + public T get() { + return value; + } + + /** + * Writes a single optional explicitly nullable value. This method is in direct relation with the + * {@link #read(StreamInput, Writeable.Reader)} which reads the respective value. It's the + * responsibility of the caller to preserve order of the fields and their backwards compatibility. + * + * @throws IOException + */ + static void write(StreamOutput out, ResettableValue value, Writeable.Writer writer) throws IOException { + out.writeBoolean(value.isDefined); + if (value.isDefined) { + out.writeBoolean(value.shouldReset()); + if (value.shouldReset() == false) { + writer.write(out, value.get()); + } + } + } + + /** + * Reads a single optional and explicitly nullable value. This method is in direct relation with the + * {@link #write(StreamOutput, ResettableValue, Writeable.Writer)} which writes the respective value. It's the + * responsibility of the caller to preserve order of the fields and their backwards compatibility. + * + * @throws IOException + */ + @Nullable + static ResettableValue read(StreamInput in, Writeable.Reader reader) throws IOException { + boolean isDefined = in.readBoolean(); + if (isDefined == false) { + return ResettableValue.undefined(); + } + boolean shouldReset = in.readBoolean(); + if (shouldReset) { + return ResettableValue.reset(); + } + T value = reader.read(in); + return ResettableValue.create(value); + } + + /** + * Gets the value and applies the function {@param f} when the value is not null. Slightly more efficient than + * this.map(f).get(). + */ + public U mapAndGet(Function f) { + if (isDefined() == false || shouldReset()) { + return null; + } else { + return f.apply(value); + } + } + + public ResettableValue map(Function mapper) { + Objects.requireNonNull(mapper); + if (isDefined == false) { + return ResettableValue.undefined(); + } + if (shouldReset()) { + return reset(); + } + return ResettableValue.create(mapper.apply(value)); + } + + /** + * Ιt merges the values of the ResettableValue's when they are defined using the provided mergeFunction. + */ + public static ResettableValue merge(ResettableValue initial, ResettableValue update, BiFunction mergeFunction) { + if (update.shouldReset()) { + return undefined(); + } + if (update.isDefined() == false) { + return initial; + } + if (initial.isDefined() == false || initial.shouldReset()) { + return update; + } + // Because we checked that's defined and not in reset state, we can directly apply the merge function. + return ResettableValue.create(mergeFunction.apply(initial.value, update.value)); + } + + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params, String field) throws IOException { + return toXContent(builder, params, field, Function.identity()); + } + + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params, String field, Function transformValue) + throws IOException { + if (isDefined) { + if (value != null) { + builder.field(field, transformValue.apply(value)); + } else if (ResettableValue.shouldDisplayResetValue(params)) { + builder.nullField(field); + } + } + return builder; + } + + public static boolean shouldDisplayResetValue(ToXContent.Params params) { + return params.paramAsBoolean(DISPLAY_RESET_VALUES, true); + } + + public static ToXContent.Params hideResetValues(ToXContent.Params params) { + return new ToXContent.DelegatingMapParams(HIDE_RESET_VALUES_PARAMS, params); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResettableValue that = (ResettableValue) o; + return isDefined == that.isDefined && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value, isDefined); + } + + @Override + public String toString() { + return "ResettableValue{" + "value=" + value + ", isDefined=" + isDefined + '}'; + } +} diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java index 0a9e79284ced..7d354768ca98 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Template.java @@ -47,12 +47,19 @@ public class Template implements SimpleDiffable