diff --git a/.travis.yml b/.travis.yml index bf66d920..482b6277 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ jdk: before_install: - sudo rm -rf /var/lib/elasticsearch - - curl https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.13.4-amd64.deb -o elasticsearch.deb && sudo dpkg -i --force-confnew elasticsearch.deb + - curl https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.14.0-amd64.deb -o elasticsearch.deb && sudo dpkg -i --force-confnew elasticsearch.deb - sudo cp ./src/test/resources/elasticsearch.yml /etc/elasticsearch/elasticsearch.yml - sudo cat /etc/elasticsearch/elasticsearch.yml - sudo java -version diff --git a/pom.xml b/pom.xml index dbe83481..57f6c641 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.nlpcn elasticsearch-sql - 8.13.4.0 + 8.14.0.0 jar Query elasticsearch using SQL elasticsearch-sql @@ -44,7 +44,7 @@ UTF-8 **/MainTestSuite.class sql - 8.13.4 + 8.14.0 org.elasticsearch.plugin.nlpcn.SqlPlug 1.2.15 32.0.0-jre diff --git a/src/main/java/org/elasticsearch/action/ParsedDocWriteResponse.java b/src/main/java/org/elasticsearch/action/ParsedDocWriteResponse.java new file mode 100644 index 00000000..29441b93 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/ParsedDocWriteResponse.java @@ -0,0 +1,72 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.action; + +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * A base class for the response of a write operation that involves a single doc + */ +public abstract class ParsedDocWriteResponse { + + /** + * Parse the output of the {@link #innerToXContent(XContentBuilder, Params)} method. + * + * This method is intended to be called by subclasses and must be called multiple times to parse all the information concerning + * {@link DocWriteResponse} objects. It always parses the current token, updates the given parsing context accordingly + * if needed and then immediately returns. + */ + public static void parseInnerToXContent(XContentParser parser, DocWriteResponse.Builder context) throws IOException { + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser); + + String currentFieldName = parser.currentName(); + token = parser.nextToken(); + + if (token.isValue()) { + if (DocWriteResponse._INDEX.equals(currentFieldName)) { + // index uuid and shard id are unknown and can't be parsed back for now. + context.setShardId(new ShardId(new Index(parser.text(), IndexMetadata.INDEX_UUID_NA_VALUE), -1)); + } else if (DocWriteResponse._ID.equals(currentFieldName)) { + context.setId(parser.text()); + } else if (DocWriteResponse._VERSION.equals(currentFieldName)) { + context.setVersion(parser.longValue()); + } else if (DocWriteResponse.RESULT.equals(currentFieldName)) { + String result = parser.text(); + for (DocWriteResponse.Result r : DocWriteResponse.Result.values()) { + if (r.getLowercase().equals(result)) { + context.setResult(r); + break; + } + } + } else if (DocWriteResponse.FORCED_REFRESH.equals(currentFieldName)) { + context.setForcedRefresh(parser.booleanValue()); + } else if (DocWriteResponse._SEQ_NO.equals(currentFieldName)) { + context.setSeqNo(parser.longValue()); + } else if (DocWriteResponse._PRIMARY_TERM.equals(currentFieldName)) { + context.setPrimaryTerm(parser.longValue()); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (DocWriteResponse._SHARDS.equals(currentFieldName)) { + context.setShardInfo(DocWriteResponse.ShardInfo.fromXContent(parser)); + } else { + parser.skipChildren(); // skip potential inner objects for forward compatibility + } + } else if (token == XContentParser.Token.START_ARRAY) { + parser.skipChildren(); // skip potential inner arrays for forward compatibility + } + } +} diff --git a/src/main/java/org/elasticsearch/action/admin/cluster/settings/ParsedClusterUpdateSettingsResponse.java b/src/main/java/org/elasticsearch/action/admin/cluster/settings/ParsedClusterUpdateSettingsResponse.java new file mode 100644 index 00000000..ff5c2387 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/admin/cluster/settings/ParsedClusterUpdateSettingsResponse.java @@ -0,0 +1,40 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.settings; + +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.XContentParser; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; + +/** + * A response for a cluster update settings action. + */ +public class ParsedClusterUpdateSettingsResponse { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cluster_update_settings_response", + true, + args -> { + return new ClusterUpdateSettingsResponse((boolean) args[0], (Settings) args[1], (Settings) args[2]); + } + ); + + static { + AcknowledgedResponse.declareAcknowledgedField(PARSER); + PARSER.declareObject(constructorArg(), (p, c) -> Settings.fromXContent(p), ClusterUpdateSettingsResponse.TRANSIENT); + PARSER.declareObject(constructorArg(), (p, c) -> Settings.fromXContent(p), ClusterUpdateSettingsResponse.PERSISTENT); + } + + public static ClusterUpdateSettingsResponse fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } +} diff --git a/src/main/java/org/elasticsearch/action/admin/indices/refresh/ParsedRefreshResponse.java b/src/main/java/org/elasticsearch/action/admin/indices/refresh/ParsedRefreshResponse.java index 43f48a05..f980e11f 100644 --- a/src/main/java/org/elasticsearch/action/admin/indices/refresh/ParsedRefreshResponse.java +++ b/src/main/java/org/elasticsearch/action/admin/indices/refresh/ParsedRefreshResponse.java @@ -8,20 +8,30 @@ package org.elasticsearch.action.admin.indices.refresh; +import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.BaseBroadcastResponse; import org.elasticsearch.action.support.broadcast.BroadcastResponse; import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; import java.util.Arrays; +import java.util.List; -import static org.elasticsearch.action.support.broadcast.BaseBroadcastResponse.declareBroadcastFields; +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * The response of a refresh action. */ public class ParsedRefreshResponse { + private static final ParseField _SHARDS_FIELD = new ParseField("_shards"); + private static final ParseField TOTAL_FIELD = new ParseField("total"); + private static final ParseField SUCCESSFUL_FIELD = new ParseField("successful"); + private static final ParseField FAILED_FIELD = new ParseField("failed"); + private static final ParseField FAILURES_FIELD = new ParseField("failures"); + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("refresh", true, arg -> { BaseBroadcastResponse response = (BaseBroadcastResponse) arg[0]; return new BroadcastResponse( @@ -39,4 +49,25 @@ public class ParsedRefreshResponse { public static BroadcastResponse fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } + + /** + * {@link BaseBroadcastResponse#declareBroadcastFields(ConstructingObjectParser)} + */ + @SuppressWarnings("unchecked") + public static void declareBroadcastFields(ConstructingObjectParser PARSER) { + ConstructingObjectParser shardsParser = new ConstructingObjectParser<>( + "_shards", + true, + arg -> new BaseBroadcastResponse((int) arg[0], (int) arg[1], (int) arg[2], (List) arg[3]) + ); + shardsParser.declareInt(constructorArg(), TOTAL_FIELD); + shardsParser.declareInt(constructorArg(), SUCCESSFUL_FIELD); + shardsParser.declareInt(constructorArg(), FAILED_FIELD); + shardsParser.declareObjectArray( + optionalConstructorArg(), + (p, c) -> DefaultShardOperationFailedException.fromXContent(p), + FAILURES_FIELD + ); + PARSER.declareObject(constructorArg(), shardsParser, _SHARDS_FIELD); + } } diff --git a/src/main/java/org/elasticsearch/action/bulk/ParsedBulkItemResponse.java b/src/main/java/org/elasticsearch/action/bulk/ParsedBulkItemResponse.java index a543f2d8..aefbfd73 100644 --- a/src/main/java/org/elasticsearch/action/bulk/ParsedBulkItemResponse.java +++ b/src/main/java/org/elasticsearch/action/bulk/ParsedBulkItemResponse.java @@ -12,7 +12,10 @@ import org.elasticsearch.action.DocWriteRequest.OpType; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.delete.DeleteResponse; +import org.elasticsearch.action.delete.ParsedDeleteResponse; import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.index.ParsedIndexResponse; +import org.elasticsearch.action.update.ParsedUpdateResponse; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.rest.RestStatus; @@ -54,17 +57,17 @@ public static BulkItemResponse fromXContent(XContentParser parser, int id) throw if (opType == OpType.INDEX || opType == OpType.CREATE) { final IndexResponse.Builder indexResponseBuilder = new IndexResponse.Builder(); builder = indexResponseBuilder; - itemParser = (indexParser) -> IndexResponse.parseXContentFields(indexParser, indexResponseBuilder); + itemParser = (indexParser) -> ParsedIndexResponse.parseXContentFields(indexParser, indexResponseBuilder); } else if (opType == OpType.UPDATE) { final UpdateResponse.Builder updateResponseBuilder = new UpdateResponse.Builder(); builder = updateResponseBuilder; - itemParser = (updateParser) -> UpdateResponse.parseXContentFields(updateParser, updateResponseBuilder); + itemParser = (updateParser) -> ParsedUpdateResponse.parseXContentFields(updateParser, updateResponseBuilder); } else if (opType == OpType.DELETE) { final DeleteResponse.Builder deleteResponseBuilder = new DeleteResponse.Builder(); builder = deleteResponseBuilder; - itemParser = (deleteParser) -> DeleteResponse.parseXContentFields(deleteParser, deleteResponseBuilder); + itemParser = (deleteParser) -> ParsedDeleteResponse.parseXContentFields(deleteParser, deleteResponseBuilder); } else { throwUnknownField(currentFieldName, parser); } diff --git a/src/main/java/org/elasticsearch/action/delete/ParsedDeleteResponse.java b/src/main/java/org/elasticsearch/action/delete/ParsedDeleteResponse.java new file mode 100644 index 00000000..63c4581f --- /dev/null +++ b/src/main/java/org/elasticsearch/action/delete/ParsedDeleteResponse.java @@ -0,0 +1,42 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.delete; + +import org.elasticsearch.action.ParsedDocWriteResponse; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * The response of the delete action. + * + * @see org.elasticsearch.action.delete.DeleteRequest + * @see org.elasticsearch.client.internal.Client#delete(DeleteRequest) + */ +public class ParsedDeleteResponse { + + public static DeleteResponse fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + + DeleteResponse.Builder context = new DeleteResponse.Builder(); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + parseXContentFields(parser, context); + } + return context.build(); + } + + /** + * Parse the current token and update the parsing context appropriately. + */ + public static void parseXContentFields(XContentParser parser, DeleteResponse.Builder context) throws IOException { + ParsedDocWriteResponse.parseInnerToXContent(parser, context); + } +} diff --git a/src/main/java/org/elasticsearch/action/index/ParsedIndexResponse.java b/src/main/java/org/elasticsearch/action/index/ParsedIndexResponse.java new file mode 100644 index 00000000..df1c0f15 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/index/ParsedIndexResponse.java @@ -0,0 +1,42 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.index; + +import org.elasticsearch.action.ParsedDocWriteResponse; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * A response of an index operation, + * + * @see org.elasticsearch.action.index.IndexRequest + * @see org.elasticsearch.client.internal.Client#index(IndexRequest) + */ +public class ParsedIndexResponse { + + public static IndexResponse fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + + IndexResponse.Builder context = new IndexResponse.Builder(); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + parseXContentFields(parser, context); + } + return context.build(); + } + + /** + * Parse the current token and update the parsing context appropriately. + */ + public static void parseXContentFields(XContentParser parser, IndexResponse.Builder context) throws IOException { + ParsedDocWriteResponse.parseInnerToXContent(parser, context); + } +} diff --git a/src/main/java/org/elasticsearch/action/search/ParsedSearchResponse.java b/src/main/java/org/elasticsearch/action/search/ParsedSearchResponse.java index ac328caf..7d882582 100644 --- a/src/main/java/org/elasticsearch/action/search/ParsedSearchResponse.java +++ b/src/main/java/org/elasticsearch/action/search/ParsedSearchResponse.java @@ -12,9 +12,12 @@ import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.action.RestActions; +import org.elasticsearch.search.ParsedSearchHits; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.profile.ParsedSearchProfileResults; import org.elasticsearch.search.profile.SearchProfileResults; +import org.elasticsearch.search.suggest.ParsedSuggest; import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParser.Token; @@ -91,13 +94,13 @@ public static SearchResponse innerFromXContent(XContentParser parser) throws IOE } } else if (token == Token.START_OBJECT) { if (SearchHits.Fields.HITS.equals(currentFieldName)) { - hits = SearchHits.fromXContent(parser); + hits = ParsedSearchHits.fromXContent(parser); } else if (InternalAggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) { aggs = InternalAggregations.fromXContent(parser); } else if (Suggest.NAME.equals(currentFieldName)) { - suggest = Suggest.fromXContent(parser); + suggest = ParsedSuggest.fromXContent(parser); } else if (SearchProfileResults.PROFILE_FIELD.equals(currentFieldName)) { - profile = SearchProfileResults.fromXContent(parser); + profile = ParsedSearchProfileResults.fromXContent(parser); } else if (RestActions._SHARDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { while ((token = parser.nextToken()) != Token.END_OBJECT) { if (token == Token.FIELD_NAME) { diff --git a/src/main/java/org/elasticsearch/action/update/ParsedUpdateResponse.java b/src/main/java/org/elasticsearch/action/update/ParsedUpdateResponse.java new file mode 100644 index 00000000..ca1272ce --- /dev/null +++ b/src/main/java/org/elasticsearch/action/update/ParsedUpdateResponse.java @@ -0,0 +1,46 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.update; + +import org.elasticsearch.action.ParsedDocWriteResponse; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +public class ParsedUpdateResponse { + + public static UpdateResponse fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + + UpdateResponse.Builder context = new UpdateResponse.Builder(); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + parseXContentFields(parser, context); + } + return context.build(); + } + + /** + * Parse the current token and update the parsing context appropriately. + */ + public static void parseXContentFields(XContentParser parser, UpdateResponse.Builder context) throws IOException { + XContentParser.Token token = parser.currentToken(); + String currentFieldName = parser.currentName(); + + if (UpdateResponse.GET.equals(currentFieldName)) { + if (token == XContentParser.Token.START_OBJECT) { + context.setGetResult(GetResult.fromXContentEmbedded(parser)); + } + } else { + ParsedDocWriteResponse.parseInnerToXContent(parser, context); + } + } +} diff --git a/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollResponse.java b/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollResponse.java index 7eed05cb..6e575e00 100644 --- a/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollResponse.java +++ b/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollResponse.java @@ -11,7 +11,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.bulk.BulkItemResponse.Failure; -import org.elasticsearch.index.reindex.BulkByScrollTask.Status; import org.elasticsearch.index.reindex.ScrollableHitSource.SearchFailure; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xcontent.ObjectParser; @@ -39,7 +38,7 @@ public class ParsedBulkByScrollResponse { PARSER.declareBoolean(BulkByScrollResponseBuilder::setTimedOut, new ParseField(BulkByScrollResponse.TIMED_OUT_FIELD)); PARSER.declareObjectArray(BulkByScrollResponseBuilder::setFailures, (p, c) -> parseFailure(p), new ParseField(BulkByScrollResponse.FAILURES_FIELD)); // since the result of BulkByScrollResponse.Status are mixed we also parse that in this - Status.declareFields(PARSER); + ParsedBulkByScrollTask.ParsedStatus.declareFields(PARSER); } public static BulkByScrollResponse fromXContent(XContentParser parser) { diff --git a/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollTask.java b/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollTask.java new file mode 100644 index 00000000..1f00506f --- /dev/null +++ b/src/main/java/org/elasticsearch/index/reindex/ParsedBulkByScrollTask.java @@ -0,0 +1,184 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParseException; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParser.Token; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; + +/** + * Task storing information about a currently running BulkByScroll request. + * + * When the request is not sliced, this task is the only task created, and starts an action to perform search requests. + * + * When the request is sliced, this task can either represent a coordinating task (using + * {@link BulkByScrollTask#setWorkerCount(int)}) or a worker task that performs search queries (using + * {@link BulkByScrollTask#setWorker(float, Integer)}). + * + * We don't always know if this task will be a leader or worker task when it's created, because if slices is set to "auto" it may + * be either depending on the number of shards in the source indices. We figure that out when the request is handled and set it on this + * class with {@link #setWorkerCount(int)} or {@link #setWorker(float, Integer)}. + */ +public class ParsedBulkByScrollTask { + + /** + * Status of the reindex, update by query, or delete by query. While in + * general we allow {@linkplain Task.Status} implementations to make + * backwards incompatible changes to their {@link Task.Status#toXContent} + * implementations, this one has become defacto standardized because Kibana + * parses it. As such, we should be very careful about removing things from + * this. + */ + public static class ParsedStatus { + + static final ConstructingObjectParser, Void> RETRIES_PARSER = new ConstructingObjectParser<>( + "bulk_by_scroll_task_status_retries", + true, + a -> new Tuple<>(((Long) a[0]), (Long) a[1]) + ); + static { + RETRIES_PARSER.declareLong(constructorArg(), new ParseField(BulkByScrollTask.Status.RETRIES_BULK_FIELD)); + RETRIES_PARSER.declareLong(constructorArg(), new ParseField(BulkByScrollTask.Status.RETRIES_SEARCH_FIELD)); + } + + public static void declareFields(ObjectParser parser) { + parser.declareInt(BulkByScrollTask.StatusBuilder::setSliceId, new ParseField(BulkByScrollTask.Status.SLICE_ID_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setTotal, new ParseField(BulkByScrollTask.Status.TOTAL_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setUpdated, new ParseField(BulkByScrollTask.Status.UPDATED_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setCreated, new ParseField(BulkByScrollTask.Status.CREATED_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setDeleted, new ParseField(BulkByScrollTask.Status.DELETED_FIELD)); + parser.declareInt(BulkByScrollTask.StatusBuilder::setBatches, new ParseField(BulkByScrollTask.Status.BATCHES_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setVersionConflicts, new ParseField(BulkByScrollTask.Status.VERSION_CONFLICTS_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setNoops, new ParseField(BulkByScrollTask.Status.NOOPS_FIELD)); + parser.declareObject(BulkByScrollTask.StatusBuilder::setRetries, RETRIES_PARSER, new ParseField(BulkByScrollTask.Status.RETRIES_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setThrottled, new ParseField(BulkByScrollTask.Status.THROTTLED_RAW_FIELD)); + parser.declareFloat(BulkByScrollTask.StatusBuilder::setRequestsPerSecond, new ParseField(BulkByScrollTask.Status.REQUESTS_PER_SEC_FIELD)); + parser.declareString(BulkByScrollTask.StatusBuilder::setReasonCancelled, new ParseField(BulkByScrollTask.Status.CANCELED_FIELD)); + parser.declareLong(BulkByScrollTask.StatusBuilder::setThrottledUntil, new ParseField(BulkByScrollTask.Status.THROTTLED_UNTIL_RAW_FIELD)); + parser.declareObjectArray( + BulkByScrollTask.StatusBuilder::setSliceStatuses, + (p, c) -> ParsedStatusOrException.fromXContent(p), + new ParseField(BulkByScrollTask.Status.SLICES_FIELD) + ); + } + + public static BulkByScrollTask.Status fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token; + if (parser.currentToken() == Token.START_OBJECT) { + token = parser.nextToken(); + } else { + token = parser.nextToken(); + } + ensureExpectedToken(Token.START_OBJECT, token, parser); + token = parser.nextToken(); + ensureExpectedToken(Token.FIELD_NAME, token, parser); + return innerFromXContent(parser); + } + + public static BulkByScrollTask.Status innerFromXContent(XContentParser parser) throws IOException { + Token token = parser.currentToken(); + String fieldName = parser.currentName(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser); + BulkByScrollTask.StatusBuilder builder = new BulkByScrollTask.StatusBuilder(); + while ((token = parser.nextToken()) != Token.END_OBJECT) { + if (token == Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == Token.START_OBJECT) { + if (fieldName.equals(BulkByScrollTask.Status.RETRIES_FIELD)) { + builder.setRetries(ParsedStatus.RETRIES_PARSER.parse(parser, null)); + } else { + parser.skipChildren(); + } + } else if (token == Token.START_ARRAY) { + if (fieldName.equals(BulkByScrollTask.Status.SLICES_FIELD)) { + while ((token = parser.nextToken()) != Token.END_ARRAY) { + builder.addToSliceStatuses(ParsedStatusOrException.fromXContent(parser)); + } + } else { + parser.skipChildren(); + } + } else { // else if it is a value + switch (fieldName) { + case BulkByScrollTask.Status.SLICE_ID_FIELD -> builder.setSliceId(parser.intValue()); + case BulkByScrollTask.Status.TOTAL_FIELD -> builder.setTotal(parser.longValue()); + case BulkByScrollTask.Status.UPDATED_FIELD -> builder.setUpdated(parser.longValue()); + case BulkByScrollTask.Status.CREATED_FIELD -> builder.setCreated(parser.longValue()); + case BulkByScrollTask.Status.DELETED_FIELD -> builder.setDeleted(parser.longValue()); + case BulkByScrollTask.Status.BATCHES_FIELD -> builder.setBatches(parser.intValue()); + case BulkByScrollTask.Status.VERSION_CONFLICTS_FIELD -> builder.setVersionConflicts(parser.longValue()); + case BulkByScrollTask.Status.NOOPS_FIELD -> builder.setNoops(parser.longValue()); + case BulkByScrollTask.Status.THROTTLED_RAW_FIELD -> builder.setThrottled(parser.longValue()); + case BulkByScrollTask.Status.REQUESTS_PER_SEC_FIELD -> builder.setRequestsPerSecond(parser.floatValue()); + case BulkByScrollTask.Status.CANCELED_FIELD -> builder.setReasonCancelled(parser.text()); + case BulkByScrollTask.Status.THROTTLED_UNTIL_RAW_FIELD -> builder.setThrottledUntil(parser.longValue()); + } + } + } + return builder.buildStatus(); + } + } + + /** + * The status of a slice of the request. Successful requests store the {@link StatusOrException#status} while failing requests store a + * {@link StatusOrException#exception}. + */ + public static class ParsedStatusOrException { + + /** + * Since {@link StatusOrException} can contain either an {@link Exception} or a {@link Status} we need to peek + * at a field first before deciding what needs to be parsed since the same object could contains either. + * The {@link #EXPECTED_EXCEPTION_FIELDS} contains the fields that are expected when the serialised object + * was an instance of exception and the {@link Status#FIELDS_SET} is the set of fields expected when the + * serialized object was an instance of Status. + */ + public static BulkByScrollTask.StatusOrException fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + if (token == Token.VALUE_NULL) { + return null; + } else { + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser); + token = parser.nextToken(); + // This loop is present only to ignore unknown tokens. It breaks as soon as we find a field + // that is allowed. + while (token != Token.END_OBJECT) { + ensureExpectedToken(Token.FIELD_NAME, token, parser); + String fieldName = parser.currentName(); + // weird way to ignore unknown tokens + if (BulkByScrollTask.Status.FIELDS_SET.contains(fieldName)) { + return new BulkByScrollTask.StatusOrException(ParsedStatus.innerFromXContent(parser)); + } else if (BulkByScrollTask.StatusOrException.EXPECTED_EXCEPTION_FIELDS.contains(fieldName)) { + return new BulkByScrollTask.StatusOrException(ElasticsearchException.innerFromXContent(parser, false)); + } else { + // Ignore unknown tokens + token = parser.nextToken(); + if (token == Token.START_OBJECT || token == Token.START_ARRAY) { + parser.skipChildren(); + } + token = parser.nextToken(); + } + } + throw new XContentParseException("Unable to parse StatusFromException. Expected fields not found."); + } + } + } +} diff --git a/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/BulkActionHandler.java b/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/BulkActionHandler.java index 241ee774..28183130 100644 --- a/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/BulkActionHandler.java +++ b/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/BulkActionHandler.java @@ -14,9 +14,9 @@ import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation; import jakarta.json.stream.JsonParser; import org.elasticsearch.action.DocWriteRequest; -import org.elasticsearch.action.bulk.BulkAction; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.bulk.ParsedBulkResponse; +import org.elasticsearch.action.bulk.TransportBulkAction; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.ActiveShardCount; @@ -47,7 +47,7 @@ public BulkActionHandler(ElasticsearchClient client) { @Override public String getName() { - return BulkAction.NAME; + return TransportBulkAction.NAME; } @Override diff --git a/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/ClusterUpdateSettingsActionHandler.java b/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/ClusterUpdateSettingsActionHandler.java index 59e97603..64ac7b5a 100644 --- a/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/ClusterUpdateSettingsActionHandler.java +++ b/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/ClusterUpdateSettingsActionHandler.java @@ -8,6 +8,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; +import org.elasticsearch.action.admin.cluster.settings.ParsedClusterUpdateSettingsResponse; import org.elasticsearch.common.Strings; import java.io.IOException; @@ -52,6 +53,6 @@ protected PutClusterSettingsRequest convertRequest(ClusterUpdateSettingsRequest @Override protected ClusterUpdateSettingsResponse convertResponse(PutClusterSettingsResponse putClusterSettingsResponse) throws IOException { - return parseJson(putClusterSettingsResponse, ClusterUpdateSettingsResponse::fromXContent); + return parseJson(putClusterSettingsResponse, ParsedClusterUpdateSettingsResponse::fromXContent); } } diff --git a/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/CreateIndexActionHandler.java b/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/CreateIndexActionHandler.java index 86b366b2..5ead5b5e 100644 --- a/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/CreateIndexActionHandler.java +++ b/src/main/java/org/elasticsearch/plugin/nlpcn/client/handler/CreateIndexActionHandler.java @@ -7,8 +7,8 @@ import co.elastic.clients.elasticsearch.indices.Alias; import co.elastic.clients.elasticsearch.indices.CreateIndexRequest; import co.elastic.clients.elasticsearch.indices.IndexSettings; -import org.elasticsearch.action.admin.indices.create.CreateIndexAction; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.admin.indices.create.TransportCreateIndexAction; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentHelper; @@ -38,7 +38,7 @@ public CreateIndexActionHandler(ElasticsearchClient client) { @Override public String getName() { - return CreateIndexAction.NAME; + return TransportCreateIndexAction.TYPE.name(); } @Override diff --git a/src/main/java/org/elasticsearch/search/ParsedSearchHit.java b/src/main/java/org/elasticsearch/search/ParsedSearchHit.java new file mode 100644 index 00000000..476cc48c --- /dev/null +++ b/src/main/java/org/elasticsearch/search/ParsedSearchHit.java @@ -0,0 +1,327 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search; + +import org.apache.lucene.search.Explanation; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.document.DocumentField; +import org.elasticsearch.common.text.Text; +import org.elasticsearch.index.mapper.IgnoredFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; +import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.ObjectParser.ValueType; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParser.Token; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName; +import static org.elasticsearch.core.RefCounted.ALWAYS_REFERENCED; +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * A single search hit. + * + * @see SearchHits + */ +public final class ParsedSearchHit { + + // All fields on the root level of the parsed SearhHit are interpreted as metadata fields + // public because we use it in a completion suggestion option + @SuppressWarnings("unchecked") + public static final ObjectParser.UnknownFieldConsumer> unknownMetaFieldConsumer = (map, fieldName, fieldValue) -> { + Map fieldMap = (Map) map.computeIfAbsent( + SearchHit.METADATA_FIELDS, + v -> new HashMap() + ); + if (fieldName.equals(IgnoredFieldMapper.NAME)) { + fieldMap.put(fieldName, new DocumentField(fieldName, (List) fieldValue)); + } else { + fieldMap.put(fieldName, new DocumentField(fieldName, Collections.singletonList(fieldValue))); + } + }; + + /** + * This parser outputs a temporary map of the objects needed to create the + * SearchHit instead of directly creating the SearchHit. The reason for this + * is that this way we can reuse the parser when parsing xContent from + * {@link org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry.Option} which unfortunately inlines + * the output of + * {@link #toInnerXContent(XContentBuilder, org.elasticsearch.xcontent.ToXContent.Params)} + * of the included search hit. The output of the map is used to create the + * actual SearchHit instance via {@link #createFromMap(Map)} + */ + private static final ObjectParser, Void> MAP_PARSER = new ObjectParser<>( + "innerHitParser", + unknownMetaFieldConsumer, + HashMap::new + ); + + static { + declareInnerHitsParseFields(MAP_PARSER); + } + + public static SearchHit fromXContent(XContentParser parser) { + return createFromMap(MAP_PARSER.apply(parser, null)); + } + + public static void declareInnerHitsParseFields(ObjectParser, Void> parser) { + parser.declareString((map, value) -> map.put(SearchHit.Fields._INDEX, value), new ParseField(SearchHit.Fields._INDEX)); + parser.declareString((map, value) -> map.put(SearchHit.Fields._ID, value), new ParseField(SearchHit.Fields._ID)); + parser.declareString((map, value) -> map.put(SearchHit.Fields._NODE, value), new ParseField(SearchHit.Fields._NODE)); + parser.declareField( + (map, value) -> map.put(SearchHit.Fields._SCORE, value), + ParsedSearchHit::parseScore, + new ParseField(SearchHit.Fields._SCORE), + ValueType.FLOAT_OR_NULL + ); + parser.declareInt((map, value) -> map.put(SearchHit.Fields._RANK, value), new ParseField(SearchHit.Fields._RANK)); + + parser.declareLong((map, value) -> map.put(SearchHit.Fields._VERSION, value), new ParseField(SearchHit.Fields._VERSION)); + parser.declareLong((map, value) -> map.put(SearchHit.Fields._SEQ_NO, value), new ParseField(SearchHit.Fields._SEQ_NO)); + parser.declareLong((map, value) -> map.put(SearchHit.Fields._PRIMARY_TERM, value), new ParseField(SearchHit.Fields._PRIMARY_TERM)); + parser.declareField( + (map, value) -> map.put(SearchHit.Fields._SHARD, value), + (p, c) -> ShardId.fromString(p.text()), + new ParseField(SearchHit.Fields._SHARD), + ValueType.STRING + ); + parser.declareObject( + (map, value) -> map.put(SourceFieldMapper.NAME, value), + (p, c) -> parseSourceBytes(p), + new ParseField(SourceFieldMapper.NAME) + ); + parser.declareObject( + (map, value) -> map.put(SearchHit.Fields.HIGHLIGHT, value), + (p, c) -> parseHighlightFields(p), + new ParseField(SearchHit.Fields.HIGHLIGHT) + ); + parser.declareObject((map, value) -> { + Map fieldMap = get(SearchHit.Fields.FIELDS, map, new HashMap()); + fieldMap.putAll(value); + map.put(SearchHit.DOCUMENT_FIELDS, fieldMap); + }, (p, c) -> parseFields(p), new ParseField(SearchHit.Fields.FIELDS)); + parser.declareObject( + (map, value) -> map.put(SearchHit.Fields._EXPLANATION, value), + (p, c) -> parseExplanation(p), + new ParseField(SearchHit.Fields._EXPLANATION) + ); + parser.declareObject( + (map, value) -> map.put(SearchHit.NestedIdentity._NESTED, value), + ParsedNestedIdentity::fromXContent, + new ParseField(SearchHit.NestedIdentity._NESTED) + ); + parser.declareObject( + (map, value) -> map.put(SearchHit.Fields.INNER_HITS, value), + (p, c) -> parseInnerHits(p), + new ParseField(SearchHit.Fields.INNER_HITS) + ); + + parser.declareField((p, map, context) -> { + XContentParser.Token token = p.currentToken(); + Map matchedQueries = new LinkedHashMap<>(); + if (token == XContentParser.Token.START_OBJECT) { + String fieldName = null; + while ((token = p.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = p.currentName(); + } else if (token.isValue()) { + matchedQueries.put(fieldName, p.floatValue()); + } + } + } else if (token == XContentParser.Token.START_ARRAY) { + while (p.nextToken() != XContentParser.Token.END_ARRAY) { + matchedQueries.put(p.text(), Float.NaN); + } + } + map.put(SearchHit.Fields.MATCHED_QUERIES, matchedQueries); + }, new ParseField(SearchHit.Fields.MATCHED_QUERIES), ObjectParser.ValueType.OBJECT_ARRAY); + + parser.declareField( + (map, list) -> map.put(SearchHit.Fields.SORT, list), + SearchSortValues::fromXContent, + new ParseField(SearchHit.Fields.SORT), + ValueType.OBJECT_ARRAY + ); + } + + public static SearchHit createFromMap(Map values) { + String id = get(SearchHit.Fields._ID, values, null); + String index = get(SearchHit.Fields._INDEX, values, null); + String clusterAlias = null; + if (index != null) { + int indexOf = index.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR); + if (indexOf > 0) { + clusterAlias = index.substring(0, indexOf); + index = index.substring(indexOf + 1); + } + } + ShardId shardId = get(SearchHit.Fields._SHARD, values, null); + String nodeId = get(SearchHit.Fields._NODE, values, null); + final SearchShardTarget shardTarget; + if (shardId != null && nodeId != null) { + assert shardId.getIndexName().equals(index); + shardTarget = new SearchShardTarget(nodeId, shardId, clusterAlias); + index = shardTarget.getIndex(); + clusterAlias = shardTarget.getClusterAlias(); + } else { + shardTarget = null; + } + return new SearchHit( + -1, + get(SearchHit.Fields._SCORE, values, SearchHit.DEFAULT_SCORE), + get(SearchHit.Fields._RANK, values, SearchHit.NO_RANK), + id == null ? null : new Text(id), + get(SearchHit.NestedIdentity._NESTED, values, null), + get(SearchHit.Fields._VERSION, values, -1L), + get(SearchHit.Fields._SEQ_NO, values, SequenceNumbers.UNASSIGNED_SEQ_NO), + get(SearchHit.Fields._PRIMARY_TERM, values, SequenceNumbers.UNASSIGNED_PRIMARY_TERM), + get(SourceFieldMapper.NAME, values, null), + get(SearchHit.Fields.HIGHLIGHT, values, null), + get(SearchHit.Fields.SORT, values, SearchSortValues.EMPTY), + get(SearchHit.Fields.MATCHED_QUERIES, values, null), + get(SearchHit.Fields._EXPLANATION, values, null), + shardTarget, + index, + clusterAlias, + null, + get(SearchHit.Fields.INNER_HITS, values, null), + get(SearchHit.DOCUMENT_FIELDS, values, Collections.emptyMap()), + get(SearchHit.METADATA_FIELDS, values, Collections.emptyMap()), + ALWAYS_REFERENCED // TODO: do we ever want pooling here? + ); + } + + @SuppressWarnings("unchecked") + private static T get(String key, Map map, T defaultValue) { + return (T) map.getOrDefault(key, defaultValue); + } + + private static float parseScore(XContentParser parser) throws IOException { + if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER || parser.currentToken() == XContentParser.Token.VALUE_STRING) { + return parser.floatValue(); + } else { + return Float.NaN; + } + } + + private static BytesReference parseSourceBytes(XContentParser parser) throws IOException { + try (XContentBuilder builder = XContentBuilder.builder(parser.contentType().xContent())) { + // the original document gets slightly modified: whitespaces or + // pretty printing are not preserved, + // it all depends on the current builder settings + builder.copyCurrentStructure(parser); + return BytesReference.bytes(builder); + } + } + + private static Map parseFields(XContentParser parser) throws IOException { + Map fields = new HashMap<>(); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + DocumentField field = DocumentField.fromXContent(parser); + fields.put(field.getName(), field); + } + return fields; + } + + private static Map parseInnerHits(XContentParser parser) throws IOException { + Map innerHits = new HashMap<>(); + while ((parser.nextToken()) != XContentParser.Token.END_OBJECT) { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); + String name = parser.currentName(); + ensureExpectedToken(Token.START_OBJECT, parser.nextToken(), parser); + ensureFieldName(parser, parser.nextToken(), SearchHits.Fields.HITS); + innerHits.put(name, ParsedSearchHits.fromXContent(parser)); + ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser); + } + return innerHits; + } + + private static Map parseHighlightFields(XContentParser parser) throws IOException { + Map highlightFields = new HashMap<>(); + while ((parser.nextToken()) != XContentParser.Token.END_OBJECT) { + HighlightField highlightField = HighlightField.fromXContent(parser); + highlightFields.put(highlightField.name(), highlightField); + } + return highlightFields; + } + + private static Explanation parseExplanation(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + XContentParser.Token token; + Float value = null; + String description = null; + List details = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser); + String currentFieldName = parser.currentName(); + token = parser.nextToken(); + if (SearchHit.Fields.VALUE.equals(currentFieldName)) { + value = parser.floatValue(); + } else if (SearchHit.Fields.DESCRIPTION.equals(currentFieldName)) { + description = parser.textOrNull(); + } else if (SearchHit.Fields.DETAILS.equals(currentFieldName)) { + ensureExpectedToken(XContentParser.Token.START_ARRAY, token, parser); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + details.add(parseExplanation(parser)); + } + } else { + parser.skipChildren(); + } + } + if (value == null) { + throw new ParsingException(parser.getTokenLocation(), "missing explanation value"); + } + if (description == null) { + throw new ParsingException(parser.getTokenLocation(), "missing explanation description"); + } + return Explanation.match(value, description, details); + } + + /** + * Encapsulates the nested identity of a hit. + */ + public static final class ParsedNestedIdentity { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "nested_identity", + true, + ctorArgs -> new SearchHit.NestedIdentity((String) ctorArgs[0], (int) ctorArgs[1], (SearchHit.NestedIdentity) ctorArgs[2]) + ); + static { + PARSER.declareString(constructorArg(), new ParseField(SearchHit.NestedIdentity.FIELD)); + PARSER.declareInt(constructorArg(), new ParseField(SearchHit.NestedIdentity.OFFSET)); + PARSER.declareObject(optionalConstructorArg(), PARSER, new ParseField(SearchHit.NestedIdentity._NESTED)); + } + + static SearchHit.NestedIdentity fromXContent(XContentParser parser, Void context) { + return fromXContent(parser); + } + + public static SearchHit.NestedIdentity fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + } +} diff --git a/src/main/java/org/elasticsearch/search/ParsedSearchHits.java b/src/main/java/org/elasticsearch/search/ParsedSearchHits.java new file mode 100644 index 00000000..fed82b16 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/ParsedSearchHits.java @@ -0,0 +1,67 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search; + +import org.apache.lucene.search.TotalHits; +import org.apache.lucene.search.TotalHits.Relation; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.search.SearchHits.parseTotalHitsFragment; + +public final class ParsedSearchHits { + + public static SearchHits fromXContent(XContentParser parser) throws IOException { + if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + parser.nextToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + } + XContentParser.Token token = parser.currentToken(); + String currentFieldName = null; + List hits = new ArrayList<>(); + TotalHits totalHits = null; + float maxScore = 0f; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (SearchHits.Fields.TOTAL.equals(currentFieldName)) { + // For BWC with nodes pre 7.0 + long value = parser.longValue(); + totalHits = value == -1 ? null : new TotalHits(value, Relation.EQUAL_TO); + } else if (SearchHits.Fields.MAX_SCORE.equals(currentFieldName)) { + maxScore = parser.floatValue(); + } + } else if (token == XContentParser.Token.VALUE_NULL) { + if (SearchHits.Fields.MAX_SCORE.equals(currentFieldName)) { + maxScore = Float.NaN; // NaN gets rendered as null-field + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (SearchHits.Fields.HITS.equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + hits.add(ParsedSearchHit.fromXContent(parser)); + } + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (SearchHits.Fields.TOTAL.equals(currentFieldName)) { + totalHits = parseTotalHitsFragment(parser); + } else { + parser.skipChildren(); + } + } + } + return SearchHits.unpooled(hits.toArray(SearchHits.EMPTY), totalHits, maxScore); + } +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java b/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java index 1ac2d03d..46cf196c 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java +++ b/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedTopHits.java @@ -9,6 +9,7 @@ package org.elasticsearch.search.aggregations.metrics; import org.elasticsearch.common.xcontent.ChunkedToXContent; +import org.elasticsearch.search.ParsedSearchHits; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.ParsedAggregation; import org.elasticsearch.xcontent.ObjectParser; @@ -46,7 +47,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th declareAggregationFields(PARSER); PARSER.declareObject( (topHit, searchHits) -> topHit.searchHits = searchHits, - (parser, context) -> SearchHits.fromXContent(parser), + (parser, context) -> ParsedSearchHits.fromXContent(parser), new ParseField(SearchHits.Fields.HITS) ); } diff --git a/src/main/java/org/elasticsearch/search/profile/ParsedSearchProfileDfsPhaseResult.java b/src/main/java/org/elasticsearch/search/profile/ParsedSearchProfileDfsPhaseResult.java new file mode 100644 index 00000000..b744172e --- /dev/null +++ b/src/main/java/org/elasticsearch/search/profile/ParsedSearchProfileDfsPhaseResult.java @@ -0,0 +1,37 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.profile; + +import org.elasticsearch.search.profile.query.ParsedQueryProfileShardResult; +import org.elasticsearch.xcontent.InstantiatingObjectParser; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class ParsedSearchProfileDfsPhaseResult { + + private static final InstantiatingObjectParser PARSER; + + static { + InstantiatingObjectParser.Builder parser = InstantiatingObjectParser.builder( + "search_profile_dfs_phase_result", + true, + SearchProfileDfsPhaseResult.class + ); + parser.declareObject(optionalConstructorArg(), (p, c) -> ProfileResult.fromXContent(p), SearchProfileDfsPhaseResult.STATISTICS); + parser.declareObjectArray(optionalConstructorArg(), (p, c) -> ParsedQueryProfileShardResult.fromXContent(p), SearchProfileDfsPhaseResult.KNN); + PARSER = parser.build(); + } + + public static SearchProfileDfsPhaseResult fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } +} diff --git a/src/main/java/org/elasticsearch/search/profile/ParsedSearchProfileResults.java b/src/main/java/org/elasticsearch/search/profile/ParsedSearchProfileResults.java new file mode 100644 index 00000000..4f0fc067 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/profile/ParsedSearchProfileResults.java @@ -0,0 +1,97 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.profile; + +import org.elasticsearch.search.profile.aggregation.AggregationProfileShardResult; +import org.elasticsearch.search.profile.query.ParsedQueryProfileShardResult; +import org.elasticsearch.search.profile.query.QueryProfileShardResult; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * Profile results for all shards. + */ +public final class ParsedSearchProfileResults { + + public static SearchProfileResults fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser); + Map profileResults = new HashMap<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.START_ARRAY) { + if (SearchProfileResults.SHARDS_FIELD.equals(parser.currentName())) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + parseProfileResultsEntry(parser, profileResults); + } + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + parser.skipChildren(); + } + } + return new SearchProfileResults(profileResults); + } + + private static void parseProfileResultsEntry(XContentParser parser, Map searchProfileResults) + throws IOException { + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser); + SearchProfileDfsPhaseResult searchProfileDfsPhaseResult = null; + List queryProfileResults = new ArrayList<>(); + AggregationProfileShardResult aggProfileShardResult = null; + ProfileResult fetchResult = null; + String id = null; + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (SearchProfileResults.ID_FIELD.equals(currentFieldName)) { + id = parser.text(); + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if ("searches".equals(currentFieldName)) { + while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { + queryProfileResults.add(ParsedQueryProfileShardResult.fromXContent(parser)); + } + } else if (AggregationProfileShardResult.AGGREGATIONS.equals(currentFieldName)) { + aggProfileShardResult = AggregationProfileShardResult.fromXContent(parser); + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if ("dfs".equals(currentFieldName)) { + searchProfileDfsPhaseResult = ParsedSearchProfileDfsPhaseResult.fromXContent(parser); + } else if ("fetch".equals(currentFieldName)) { + fetchResult = ProfileResult.fromXContent(parser); + } else { + parser.skipChildren(); + } + } else { + parser.skipChildren(); + } + } + SearchProfileShardResult result = new SearchProfileShardResult( + new SearchProfileQueryPhaseResult(queryProfileResults, aggProfileShardResult), + fetchResult + ); + result.getQueryPhase().setSearchProfileDfsPhaseResult(searchProfileDfsPhaseResult); + searchProfileResults.put(id, result); + } +} diff --git a/src/main/java/org/elasticsearch/search/profile/query/ParsedQueryProfileShardResult.java b/src/main/java/org/elasticsearch/search/profile/query/ParsedQueryProfileShardResult.java new file mode 100644 index 00000000..67d39f6f --- /dev/null +++ b/src/main/java/org/elasticsearch/search/profile/query/ParsedQueryProfileShardResult.java @@ -0,0 +1,63 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.profile.query; + +import org.elasticsearch.search.profile.ProfileResult; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * A container class to hold the profile results for a single shard in the request. + * Contains a list of query profiles, a collector tree and a total rewrite tree. + */ +public final class ParsedQueryProfileShardResult { + + public static QueryProfileShardResult fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser); + String currentFieldName = null; + List queryProfileResults = new ArrayList<>(); + long rewriteTime = 0; + Long vectorOperationsCount = null; + CollectorResult collector = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (QueryProfileShardResult.REWRITE_TIME.equals(currentFieldName)) { + rewriteTime = parser.longValue(); + } else if (QueryProfileShardResult.VECTOR_OPERATIONS_COUNT.equals(currentFieldName)) { + vectorOperationsCount = parser.longValue(); + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (QueryProfileShardResult.QUERY_ARRAY.equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + queryProfileResults.add(ProfileResult.fromXContent(parser)); + } + } else if (QueryProfileShardResult.COLLECTOR.equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + collector = CollectorResult.fromXContent(parser); + } + } else { + parser.skipChildren(); + } + } else { + parser.skipChildren(); + } + } + return new QueryProfileShardResult(queryProfileResults, rewriteTime, collector, vectorOperationsCount); + } +} diff --git a/src/main/java/org/elasticsearch/search/suggest/ParsedSuggest.java b/src/main/java/org/elasticsearch/search/suggest/ParsedSuggest.java new file mode 100644 index 00000000..95478041 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/suggest/ParsedSuggest.java @@ -0,0 +1,79 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.search.suggest; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry; +import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * Top level suggest result, containing the result for each suggestion. + */ +public final class ParsedSuggest { + + /** + * this parsing method assumes that the leading "suggest" field name has already been parsed by the caller + */ + public static Suggest fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + List>> suggestions = new ArrayList<>(); + while ((parser.nextToken()) != XContentParser.Token.END_OBJECT) { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); + String currentField = parser.currentName(); + ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser); + Suggest.Suggestion> suggestion = ParsedSuggestion.fromXContent(parser); + if (suggestion != null) { + suggestions.add(suggestion); + } else { + throw new ParsingException( + parser.getTokenLocation(), + String.format(Locale.ROOT, "Could not parse suggestion keyed as [%s]", currentField) + ); + } + } + return new Suggest(suggestions); + } + + /** + * The suggestion responses corresponding with the suggestions in the request. + */ + @SuppressWarnings("rawtypes") + public abstract static class ParsedSuggestion { + + @SuppressWarnings("unchecked") + public static Suggest.Suggestion> fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser); + SetOnce suggestion = new SetOnce<>(); + XContentParserUtils.parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, Suggest.Suggestion.class, suggestion::set); + return suggestion.get(); + } + + protected static > void parseEntries( + XContentParser parser, + Suggest.Suggestion suggestion, + CheckedFunction entryParser + ) throws IOException { + ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser); + while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { + suggestion.addTerm(entryParser.apply(parser)); + } + } + } +}