diff --git a/client/client-benchmark-noop-api-plugin/src/main/java/org/elasticsearch/plugin/noop/action/search/TransportNoopSearchAction.java b/client/client-benchmark-noop-api-plugin/src/main/java/org/elasticsearch/plugin/noop/action/search/TransportNoopSearchAction.java index 66a0e785af9ea..08fc46a029c2c 100644 --- a/client/client-benchmark-noop-api-plugin/src/main/java/org/elasticsearch/plugin/noop/action/search/TransportNoopSearchAction.java +++ b/client/client-benchmark-noop-api-plugin/src/main/java/org/elasticsearch/plugin/noop/action/search/TransportNoopSearchAction.java @@ -9,12 +9,12 @@ package org.elasticsearch.plugin.noop.action.search; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.search.PhaseFailure; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.injection.guice.Inject; import org.elasticsearch.plugin.noop.NoopPlugin; @@ -34,7 +34,7 @@ public TransportNoopSearchAction(TransportService transportService, ActionFilter NoopPlugin.NOOP_SEARCH_ACTION.name(), transportService, actionFilters, - (Writeable.Reader) SearchRequest::new, + SearchRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE ); } @@ -56,6 +56,7 @@ protected void doExecute(Task task, SearchRequest request, ActionListener exten private final AtomicInteger successfulOps; private final SearchTimeProvider timeProvider; private final SearchResponse.Clusters clusters; + private final List phaseFailures = Collections.synchronizedList(new ArrayList<>()); protected final List shardsIts; private final SearchShardIterator[] shardIterators; @@ -572,6 +574,7 @@ public boolean isPartOfPointInTime(ShardSearchContextId contextId) { private SearchResponse buildSearchResponse( SearchResponseSections internalSearchResponse, ShardSearchFailure[] failures, + PhaseFailure[] phaseFailures, String scrollId, BytesReference searchContextId ) { @@ -587,6 +590,7 @@ private SearchResponse buildSearchResponse( skippedCount, buildTookInMillis(), failures, + phaseFailures, clusters, searchContextId ); @@ -622,7 +626,13 @@ public void sendSearchResponse(SearchResponseSections internalSearchResponse, At searchContextId = null; } } - ActionListener.respondAndRelease(listener, buildSearchResponse(internalSearchResponse, failures, scrollId, searchContextId)); + + PhaseFailure[] subFailures = phaseFailures.toArray(PhaseFailure.EMPTY_ARRAY); + + ActionListener.respondAndRelease( + listener, + buildSearchResponse(internalSearchResponse, failures, subFailures, scrollId, searchContextId) + ); } } @@ -660,6 +670,13 @@ private void raisePhaseFailure(SearchPhaseExecutionException exception) { listener.onFailure(exception); } + /** + * Records a failure of a specific search phase, that does not cause the search operation itself to fail. + */ + public void addPhaseFailure(String phase, Exception exception) { + phaseFailures.add(new PhaseFailure(phase, exception)); + } + /** * Releases a search context with the given context ID on the node the given connection is connected to. * @see org.elasticsearch.search.query.QuerySearchResult#getContextId() diff --git a/server/src/main/java/org/elasticsearch/action/search/PhaseFailure.java b/server/src/main/java/org/elasticsearch/action/search/PhaseFailure.java new file mode 100644 index 0000000000000..6e29c0f260b14 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/search/PhaseFailure.java @@ -0,0 +1,51 @@ +/* + * 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.action.search; + +import org.elasticsearch.ElasticsearchException; +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.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Contains information on a failure of a particular search phase (ie not a shard failure), + * that does not cause the search operation itself to fail. + */ +public record PhaseFailure(String phase, Exception failure) implements ToXContentObject, Writeable { + public static final String PHASE_FIELD = "phase"; + public static final String FAILURE_FIELD = "failure"; + + public static final PhaseFailure[] EMPTY_ARRAY = new PhaseFailure[0]; + + public PhaseFailure(StreamInput in) throws IOException { + this(in.readString(), in.readException()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(phase); + out.writeException(failure); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(PHASE_FIELD, phase); + builder.startObject(FAILURE_FIELD); + ElasticsearchException.generateThrowableXContent(builder, params, failure); + builder.endObject(); + builder.endObject(); + return builder; + } +} diff --git a/server/src/main/java/org/elasticsearch/action/search/RankFeaturePhase.java b/server/src/main/java/org/elasticsearch/action/search/RankFeaturePhase.java index 25238711c5c1c..3d6030a0a7f83 100644 --- a/server/src/main/java/org/elasticsearch/action/search/RankFeaturePhase.java +++ b/server/src/main/java/org/elasticsearch/action/search/RankFeaturePhase.java @@ -199,9 +199,10 @@ public void onResponse(RankFeatureDoc[] docsWithUpdatedScores) { @Override public void onFailure(Exception e) { if (rankFeaturePhaseRankCoordinatorContext.failuresAllowed()) { - // TODO: handle the exception somewhere // don't want to log the entire stack trace, it's not helpful here logger.warn("Exception computing updated ranks, continuing with existing ranks: {}", e.toString()); + context.addPhaseFailure("reranking", e); + // use the existing score docs as-is // downstream things expect every doc to have a score, so we need to infer a score here // if the doc doesn't otherwise have one. We can use the rank to infer a possible score instead (1/rank). diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java index 1f676f29e446e..e82c0323db17f 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -20,6 +20,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.common.xcontent.ChunkedToXContentObject; @@ -54,8 +55,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; -import static org.elasticsearch.action.search.ShardSearchFailure.readShardSearchFailure; - /** * A response of a search request. */ @@ -71,6 +70,7 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO public static final ParseField TIMED_OUT = new ParseField("timed_out"); public static final ParseField TERMINATED_EARLY = new ParseField("terminated_early"); public static final ParseField NUM_REDUCE_PHASES = new ParseField("num_reduce_phases"); + public static final ParseField PHASE_FAILURES = new ParseField("phase_failures"); private final SearchHits hits; private final InternalAggregations aggregations; @@ -85,6 +85,7 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO private final int successfulShards; private final int skippedShards; private final ShardSearchFailure[] shardFailures; + private final PhaseFailure[] phaseFailures; private final Clusters clusters; private final long tookInMillis; @@ -108,14 +109,14 @@ public SearchResponse(StreamInput in) throws IOException { this.numReducePhases = in.readVInt(); totalShards = in.readVInt(); successfulShards = in.readVInt(); - int size = in.readVInt(); - if (size == 0) { - shardFailures = ShardSearchFailure.EMPTY_ARRAY; + shardFailures = in.readArray( + ShardSearchFailure::readShardSearchFailure, + s -> s == 0 ? ShardSearchFailure.EMPTY_ARRAY : new ShardSearchFailure[s] + ); + if (in.getTransportVersion().onOrAfter(TransportVersions.SEARCH_PHASE_FAILURES)) { + phaseFailures = in.readArray(PhaseFailure::new, s -> s == 0 ? PhaseFailure.EMPTY_ARRAY : new PhaseFailure[s]); } else { - shardFailures = new ShardSearchFailure[size]; - for (int i = 0; i < shardFailures.length; i++) { - shardFailures[i] = readShardSearchFailure(in); - } + phaseFailures = PhaseFailure.EMPTY_ARRAY; } clusters = new Clusters(in); scrollId = in.readOptionalString(); @@ -138,6 +139,7 @@ public SearchResponse( int skippedShards, long tookInMillis, ShardSearchFailure[] shardFailures, + PhaseFailure[] phaseFailures, Clusters clusters ) { this( @@ -154,6 +156,7 @@ public SearchResponse( skippedShards, tookInMillis, shardFailures, + phaseFailures, clusters, null ); @@ -167,6 +170,7 @@ public SearchResponse( int skippedShards, long tookInMillis, ShardSearchFailure[] shardFailures, + PhaseFailure[] phaseFailures, Clusters clusters, BytesReference pointInTimeId ) { @@ -184,6 +188,7 @@ public SearchResponse( skippedShards, tookInMillis, shardFailures, + phaseFailures, clusters, pointInTimeId ); @@ -203,6 +208,7 @@ public SearchResponse( int skippedShards, long tookInMillis, ShardSearchFailure[] shardFailures, + PhaseFailure[] phaseFailures, Clusters clusters, BytesReference pointInTimeId ) { @@ -222,6 +228,7 @@ public SearchResponse( this.skippedShards = skippedShards; this.tookInMillis = tookInMillis; this.shardFailures = shardFailures; + this.phaseFailures = phaseFailures; assert skippedShards <= totalShards : "skipped: " + skippedShards + " total: " + totalShards; assert scrollId == null || pointInTimeId == null : "SearchResponse can't have both scrollId [" + scrollId + "] and searchContextId [" + pointInTimeId + "]"; @@ -347,7 +354,14 @@ public int getFailedShards() { * The failures that occurred during the search. */ public ShardSearchFailure[] getShardFailures() { - return this.shardFailures; + return shardFailures; + } + + /** + * The failures that occurred with specific phases that did not stop results from being returned. + */ + public PhaseFailure[] getPhaseFailures() { + return phaseFailures; } /** @@ -401,6 +415,9 @@ private Iterator getToXContentIterator(boolean wrapInObject, ToXCont return Iterators.concat( wrapInObject ? ChunkedToXContentHelper.startObject() : Collections.emptyIterator(), ChunkedToXContentHelper.chunk(SearchResponse.this::headerToXContent), + CollectionUtils.isEmpty(phaseFailures) + ? Collections.emptyIterator() + : ChunkedToXContentHelper.array(PHASE_FAILURES.getPreferredName(), Iterators.forArray(phaseFailures)), Iterators.single(clusters), hits.toXContentChunked(params), aggregations == null ? Collections.emptyIterator() : ChunkedToXContentHelper.chunk(aggregations), @@ -452,10 +469,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVInt(numReducePhases); out.writeVInt(totalShards); out.writeVInt(successfulShards); - - out.writeVInt(shardFailures.length); - for (ShardSearchFailure shardSearchFailure : shardFailures) { - shardSearchFailure.writeTo(out); + out.writeArray(shardFailures); + if (out.getTransportVersion().onOrAfter(TransportVersions.SEARCH_PHASE_FAILURES)) { + out.writeArray(phaseFailures); } clusters.writeTo(out); out.writeOptionalString(scrollId); @@ -1149,6 +1165,7 @@ public static SearchResponse empty(Supplier tookInMillisSupplier, Clusters 0, tookInMillisSupplier.get(), ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, clusters, null ); diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java index bbb4ce45c47dc..d878afad65b14 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java @@ -129,6 +129,7 @@ public SearchResponse getMergedResponse(Clusters clusters) { // the current reduce phase counts as one int numReducePhases = 1; List failures = new ArrayList<>(); + List phaseFailures = new ArrayList<>(); Map profileResults = new HashMap<>(); List aggs = new ArrayList<>(); Map shards = new TreeMap<>(); @@ -145,6 +146,7 @@ public SearchResponse getMergedResponse(Clusters clusters) { numReducePhases += searchResponse.getNumReducePhases(); Collections.addAll(failures, searchResponse.getShardFailures()); + Collections.addAll(phaseFailures, searchResponse.getPhaseFailures()); profileResults.putAll(searchResponse.getProfileResults()); @@ -213,6 +215,7 @@ public SearchResponse getMergedResponse(Clusters clusters) { ? InternalAggregations.EMPTY : InternalAggregations.topLevelReduce(aggs, aggReduceContextBuilder.forFinalReduction()); ShardSearchFailure[] shardFailures = failures.toArray(ShardSearchFailure.EMPTY_ARRAY); + PhaseFailure[] subFailures = phaseFailures.toArray(PhaseFailure.EMPTY_ARRAY); SearchProfileResults profileShardResults = profileResults.isEmpty() ? null : new SearchProfileResults(profileResults); // make failures ordering consistent between ordinary search and CCS by looking at the shard they come from Arrays.sort(shardFailures, FAILURES_COMPARATOR); @@ -231,6 +234,7 @@ public SearchResponse getMergedResponse(Clusters clusters) { skippedShards, tookInMillis, shardFailures, + subFailures, clusters, null ); diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchScrollAsyncAction.java b/server/src/main/java/org/elasticsearch/action/search/SearchScrollAsyncAction.java index 9d3ed7cf5fa96..fcf385f23b8f5 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchScrollAsyncAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchScrollAsyncAction.java @@ -262,6 +262,7 @@ protected final void sendResponse( 0, buildTookInMillis(), buildShardFailures(), + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY, null ) 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 8e0806f0fa8e3..5a334d24eac44 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -698,6 +698,7 @@ public void onResponse(SearchResponse searchResponse) { searchResponse.getSkippedShards(), timeProvider.buildTookInMillis(), searchResponse.getShardFailures(), + searchResponse.getPhaseFailures(), clusters, searchResponse.pointInTimeId() ) diff --git a/server/src/test/java/org/elasticsearch/action/search/FetchLookupFieldsPhaseTests.java b/server/src/test/java/org/elasticsearch/action/search/FetchLookupFieldsPhaseTests.java index 5c508dce61fc3..d00ded57d5078 100644 --- a/server/src/test/java/org/elasticsearch/action/search/FetchLookupFieldsPhaseTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/FetchLookupFieldsPhaseTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHitTests; import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.SearchResponseUtils; import org.elasticsearch.search.fetch.subphase.FieldAndFormat; import org.elasticsearch.search.fetch.subphase.LookupField; import org.elasticsearch.test.ESTestCase; @@ -106,26 +107,7 @@ void sendExecuteMultiSearch( } else { searchHits = SearchHits.empty(new TotalHits(0, TotalHits.Relation.EQUAL_TO), 1.0f); } - responses[i] = new MultiSearchResponse.Item( - new SearchResponse( - searchHits, - null, - null, - false, - null, - null, - 1, - null, - 1, - 1, - 0, - randomNonNegativeLong(), - ShardSearchFailure.EMPTY_ARRAY, - SearchResponseTests.randomClusters(), - null - ), - null - ); + responses[i] = new MultiSearchResponse.Item(SearchResponseUtils.successfulResponse(searchHits), null); searchHits.decRef(); } ActionListener.respondAndRelease(listener, new MultiSearchResponse(responses, randomNonNegativeLong())); diff --git a/server/src/test/java/org/elasticsearch/action/search/MockSearchPhaseContext.java b/server/src/test/java/org/elasticsearch/action/search/MockSearchPhaseContext.java index 16e90064ca929..72caeaac1dfdc 100644 --- a/server/src/test/java/org/elasticsearch/action/search/MockSearchPhaseContext.java +++ b/server/src/test/java/org/elasticsearch/action/search/MockSearchPhaseContext.java @@ -44,6 +44,7 @@ public final class MockSearchPhaseContext extends AbstractSearchAsyncAction failures = Collections.synchronizedList(new ArrayList<>()); + public final List phaseFailures = Collections.synchronizedList(new ArrayList<>()); SearchTransportService searchTransport; final Set releasedSearchContexts = new HashSet<>(); public final AtomicReference searchResponse = new AtomicReference<>(); @@ -99,6 +100,7 @@ public void sendSearchResponse(SearchResponseSections internalSearchResponse, At 0, 0, failures.toArray(ShardSearchFailure.EMPTY_ARRAY), + phaseFailures.toArray(PhaseFailure.EMPTY_ARRAY), SearchResponse.Clusters.EMPTY, searchContextId ) @@ -120,6 +122,11 @@ public void onShardFailure(int shardIndex, @Nullable SearchShardTarget shardTarg numSuccess.decrementAndGet(); } + @Override + public void addPhaseFailure(String phase, Exception exception) { + phaseFailures.add(new PhaseFailure(phase, exception)); + } + @Override protected SearchPhase getNextPhase() { return null; diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchAsyncActionTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchAsyncActionTests.java index afd3bee4c4ab8..794c273288766 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchAsyncActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchAsyncActionTests.java @@ -783,6 +783,7 @@ public static class TestSearchResponse extends SearchResponse { 0, 0L, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, Clusters.EMPTY, null ); diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchResponseMergerTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchResponseMergerTests.java index d54ac9c66d9a5..d3fa47049e582 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchResponseMergerTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchResponseMergerTests.java @@ -60,6 +60,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.elasticsearch.test.InternalAggregationTestCase.emptyReduceContextBuilder; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -135,6 +136,60 @@ public void testMergeTookInMillis() throws InterruptedException { } } + public void testMergePhaseFailures() throws InterruptedException { + SearchTimeProvider searchTimeProvider = new SearchTimeProvider(0, 0, () -> 0); + try ( + SearchResponseMerger merger = new SearchResponseMerger( + 0, + 0, + SearchContext.TRACK_TOTAL_HITS_ACCURATE, + searchTimeProvider, + emptyReduceContextBuilder() + ) + ) { + List allFailures = new ArrayList<>(); + + for (int i = 0; i < numResponses; i++) { + int numFailures = randomIntBetween(0, 3); + PhaseFailure[] phaseFailures = new PhaseFailure[numFailures]; + for (int j = 0; j < numFailures; j++) { + phaseFailures[j] = new PhaseFailure(randomAlphaOfLength(5), new IllegalArgumentException()); + } + Collections.addAll(allFailures, phaseFailures); + SearchResponse searchResponse = new SearchResponse( + SearchHits.EMPTY_WITH_TOTAL_HITS, + null, + null, + false, + null, + null, + 1, + null, + 1, + 1, + 0, + 100L, + ShardSearchFailure.EMPTY_ARRAY, + phaseFailures, + SearchResponse.Clusters.EMPTY + ); + try { + addResponse(merger, searchResponse); + } finally { + searchResponse.decRef(); + } + } + awaitResponsesAdded(); + assertEquals(numResponses, merger.numResponses()); + SearchResponse mergedResponse = merger.getMergedResponse(SearchResponse.Clusters.EMPTY); + try { + assertThat(mergedResponse.getPhaseFailures(), arrayContainingInAnyOrder(allFailures.toArray())); + } finally { + mergedResponse.decRef(); + } + } + } + public void testMergeShardFailures() throws InterruptedException { SearchTimeProvider searchTimeProvider = new SearchTimeProvider(0, 0, () -> 0); try ( @@ -346,6 +401,7 @@ public void testMergeProfileResults() throws InterruptedException { 0, 100L, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -424,6 +480,7 @@ public void testMergeCompletionSuggestions() throws InterruptedException { 0, randomLong(), ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -509,6 +566,7 @@ public void testMergeCompletionSuggestionsTieBreak() throws InterruptedException 0, randomLong(), ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -590,6 +648,7 @@ public void testMergeEmptyFormat() throws InterruptedException { 0, randomLong(), ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -657,6 +716,7 @@ public void testMergeAggs() throws InterruptedException { 0, randomLong(), ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); @@ -820,6 +880,7 @@ public void testMergeSearchHits() throws InterruptedException { skipped, randomLong(), ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponseTests.randomClusters() ); @@ -968,6 +1029,7 @@ public void testMergeEmptySearchHitsWithNonEmpty() { 0, 1L, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -992,6 +1054,7 @@ public void testMergeEmptySearchHitsWithNonEmpty() { 0, 1L, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -1048,6 +1111,7 @@ public void testMergeOnlyEmptyHits() { 0, 1L, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { @@ -1149,6 +1213,7 @@ public void testPartialAggsMixedWithFullResponses() { 0, 33, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); @@ -1174,6 +1239,7 @@ public void testPartialAggsMixedWithFullResponses() { skipped, 44, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); @@ -1199,6 +1265,7 @@ public void testPartialAggsMixedWithFullResponses() { skipped, 55, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); try { diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java index bbeae6b19b8ac..95e92e748a2a7 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java @@ -10,6 +10,7 @@ package org.elasticsearch.action.search; import org.apache.lucene.search.TotalHits; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.TransportVersion; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.support.IndicesOptions; @@ -27,9 +28,8 @@ import org.elasticsearch.search.SearchHitsTests; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.SearchResponseUtils; -import org.elasticsearch.search.profile.SearchProfileResults; +import org.elasticsearch.search.SearchResponseUtils.SearchResponseBuilder; import org.elasticsearch.search.profile.SearchProfileResultsTests; -import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.search.suggest.SuggestTests; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -45,11 +45,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import static java.util.Collections.emptyList; import static java.util.Collections.singletonMap; import static org.elasticsearch.test.XContentTestUtils.insertRandomFields; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class SearchResponseTests extends ESTestCase { @@ -68,69 +72,40 @@ protected NamedXContentRegistry xContentRegistry() { return xContentRegistry; } - private SearchResponse createTestItem(ShardSearchFailure... shardSearchFailures) { - return createTestItem(false, shardSearchFailures); + private static SearchResponse createTestItem(ShardSearchFailure... shardSearchFailures) { + return createTestItem(false).shardFailures(shardSearchFailures).build(); } /** * This SearchResponse doesn't include SearchHits, Aggregations, Suggestions, ShardSearchFailures, SearchProfileShardResults * to make it possible to only test properties of the SearchResponse itself */ - private SearchResponse createMinimalTestItem() { - return createTestItem(true); + private static SearchResponse createMinimalTestItem() { + return createTestItem(true).build(); } /** * if minimal is set, don't include search hits, aggregations, suggest etc... to make test simpler */ - private SearchResponse createTestItem(boolean minimal, ShardSearchFailure... shardSearchFailures) { - boolean timedOut = randomBoolean(); - Boolean terminatedEarly = randomBoolean() ? null : randomBoolean(); - int numReducePhases = randomIntBetween(1, 10); - long tookInMillis = randomNonNegativeLong(); - int totalShards = randomIntBetween(1, Integer.MAX_VALUE); - int successfulShards = randomIntBetween(0, totalShards); - int skippedShards = randomIntBetween(0, totalShards); - SearchResponse.Clusters clusters; - if (minimal) { - clusters = randomSimpleClusters(); - } else { - clusters = randomClusters(); - } + private static SearchResponseBuilder createTestItem(boolean minimal) { + int shards = randomIntBetween(1, Integer.MAX_VALUE); + SearchResponseBuilder builder = SearchResponseUtils.response() + .timedOut(randomBoolean()) + .terminatedEarly(randomBoolean() ? null : randomBoolean()) + .numReducePhases(randomIntBetween(1, 10)) + .tookInMillis(randomNonNegativeLong()) + .shards(shards, randomIntBetween(0, shards), randomIntBetween(0, shards)) + .clusters(minimal ? randomSimpleClusters() : randomClusters()); + if (minimal == false) { SearchHits hits = SearchHitsTests.createTestItem(true, true); try { - Suggest suggest = SuggestTests.createTestItem(); - SearchProfileResults profileResults = SearchProfileResultsTests.createTestItem(); - return new SearchResponse( - hits, - null, - suggest, - timedOut, - terminatedEarly, - profileResults, - numReducePhases, - null, - totalShards, - successfulShards, - skippedShards, - tookInMillis, - shardSearchFailures, - clusters - ); + return builder.suggest(SuggestTests.createTestItem()).profileResults(SearchProfileResultsTests.createTestItem()); } finally { hits.decRef(); } } else { - return SearchResponseUtils.emptyWithTotalHits( - null, - totalShards, - successfulShards, - skippedShards, - tookInMillis, - shardSearchFailures, - clusters - ); + return builder.searchHits(SearchHits.EMPTY_WITH_TOTAL_HITS); } } @@ -321,7 +296,7 @@ private void doFromXContentTestWithRandomFields(SearchResponse response, boolean * Because of this, in this special test case we compare the "top level" fields for equality * and the subsections xContent equivalence independently */ - public void testFromXContentWithFailures() throws IOException { + public void testFromXContentWithShardFailures() throws IOException { int numFailures = randomIntBetween(1, 5); ShardSearchFailure[] failures = new ShardSearchFailure[numFailures]; for (int i = 0; i < failures.length; i++) { @@ -364,6 +339,56 @@ public void testFromXContentWithFailures() throws IOException { } } + /** + * Exceptions can be wrapped in XContent, which makes direct comparisons difficult. + * Because of this, in this special test case we compare the "top level" fields for equality + * and the subsections xContent equivalence independently + */ + public void testFromXContentWithPhaseFailures() throws IOException { + int numFailures = randomIntBetween(1, 5); + PhaseFailure[] failures = new PhaseFailure[numFailures]; + for (int i = 0; i < failures.length; i++) { + failures[i] = new PhaseFailure( + randomAlphaOfLengthBetween(2, 10), + ESTestCase.>randomFrom( + IllegalArgumentException::new, + IOException::new, + ElasticsearchTimeoutException::new + ).apply(randomAlphaOfLength(20)) + ); + } + BytesReference originalBytes; + SearchResponse response = createTestItem(false).phaseFailures(failures).build(); + XContentType xcontentType = randomFrom(XContentType.values()); + try { + final ToXContent.Params params = new ToXContent.MapParams(singletonMap(RestSearchAction.TYPED_KEYS_PARAM, "true")); + originalBytes = toShuffledXContent(ChunkedToXContent.wrapAsToXContent(response), xcontentType, params, randomBoolean()); + } finally { + response.decRef(); + } + try (XContentParser parser = createParser(xcontentType.xContent(), originalBytes)) { + SearchResponse parsed = SearchResponseUtils.parseSearchResponse(parser); + try { + PhaseFailure[] deserFailures = parsed.getPhaseFailures(); + for (int i = 0; i < deserFailures.length; i++) { + PhaseFailure deserFailure = deserFailures[i]; + assertThat(deserFailure.phase(), equalTo(failures[i].phase())); + assertThat( + deserFailure.failure().getMessage(), + allOf( + containsString(ElasticsearchTimeoutException.getExceptionName(failures[i].failure())), + containsString(failures[i].failure().getMessage()) + ) + ); + } + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertNull(parser.nextToken()); + } finally { + parsed.decRef(); + } + } + } + public void testToXContent() throws IOException { SearchHit hit = new SearchHit(1, "id1"); hit.score(2.0f); @@ -384,6 +409,50 @@ public void testToXContent() throws IOException { 0, 0, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, + SearchResponse.Clusters.EMPTY + ); + try { + String expectedString = XContentHelper.stripWhitespace(""" + { + "took": 0, + "timed_out": false, + "_shards": { + "total": 0, + "successful": 0, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 100, + "relation": "eq" + }, + "max_score": 1.5, + "hits": [ { "_id": "id1", "_score": 2.0 } ] + } + }"""); + assertEquals(expectedString, Strings.toString(response)); + } finally { + response.decRef(); + } + } + { + SearchResponse response = new SearchResponse( + sHits, + null, + null, + false, + null, + null, + 1, + null, + 0, + 0, + 0, + 0, + ShardSearchFailure.EMPTY_ARRAY, + new PhaseFailure[] { new PhaseFailure("rescore", new IOException("io")) }, SearchResponse.Clusters.EMPTY ); try { @@ -397,6 +466,9 @@ public void testToXContent() throws IOException { "skipped": 0, "failed": 0 }, + "phase_failures": [ { + "phase": "rescore", "failure": {"type":"i_o_exception", "reason":"io"} + } ], "hits": { "total": { "value": 100, @@ -426,6 +498,7 @@ public void testToXContent() throws IOException { 0, 0, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, new SearchResponse.Clusters(5, 3, 2) ); try { @@ -476,6 +549,7 @@ public void testToXContent() throws IOException { 2, 0, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, createCCSClusterObject( 4, 3, @@ -609,7 +683,7 @@ public void testToXContent() throws IOException { } public void testSerialization() throws IOException { - SearchResponse searchResponse = createTestItem(false); + SearchResponse searchResponse = createTestItem(false).build(); try { SearchResponse deserialized = copyWriteable( searchResponse, diff --git a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java index 7ab9b8611b8c4..289f1317c2e9e 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -68,9 +68,9 @@ import org.elasticsearch.rest.action.search.SearchResponseMetrics; import org.elasticsearch.search.DummyQueryBuilder; import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.SearchResponseUtils; import org.elasticsearch.search.SearchService; import org.elasticsearch.search.SearchShardTarget; -import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.collapse.CollapseBuilder; import org.elasticsearch.search.internal.AliasFilter; @@ -1026,23 +1026,7 @@ public void onNodeDisconnected(DiscoveryNode node, Transport.Connection connecti private static void resolveWithEmptySearchResponse(Tuple> tuple) { ActionListener.respondAndRelease( tuple.v2(), - new SearchResponse( - SearchHits.empty(new TotalHits(0, TotalHits.Relation.EQUAL_TO), Float.NaN), - InternalAggregations.EMPTY, - null, - false, - null, - null, - 1, - null, - 1, - 1, - 0, - 100, - ShardSearchFailure.EMPTY_ARRAY, - SearchResponse.Clusters.EMPTY, - null - ) + SearchResponseUtils.successfulResponse(SearchHits.empty(new TotalHits(0, TotalHits.Relation.EQUAL_TO), Float.NaN)) ); } diff --git a/test/framework/src/main/java/org/elasticsearch/search/SearchResponseUtils.java b/test/framework/src/main/java/org/elasticsearch/search/SearchResponseUtils.java index 0321550736660..fc4a6d8b38fda 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/SearchResponseUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/search/SearchResponseUtils.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.search.MultiSearchResponse; +import org.elasticsearch.action.search.PhaseFailure; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponse.Clusters; @@ -113,6 +114,7 @@ public static SearchResponse emptyWithTotalHits( skippedShards, tookInMillis, shardFailures, + PhaseFailure.EMPTY_ARRAY, clusters ); } @@ -131,6 +133,7 @@ public static class SearchResponseBuilder { private int skippedShards; private long tookInMillis; private List shardFailures; + private List phaseFailures; private Clusters clusters = Clusters.EMPTY; private BytesReference pointInTimeId; @@ -198,6 +201,16 @@ public SearchResponseBuilder shardFailures(List failures) { return this; } + public SearchResponseBuilder phaseFailures(PhaseFailure... failures) { + phaseFailures = List.of(failures); + return this; + } + + public SearchResponseBuilder phaseFailures(List failures) { + phaseFailures = List.copyOf(failures); + return this; + } + public SearchResponseBuilder clusters(Clusters clusters) { this.clusters = clusters; return this; @@ -223,6 +236,7 @@ public SearchResponse build() { skippedShards, tookInMillis, shardFailures == null ? ShardSearchFailure.EMPTY_ARRAY : shardFailures.toArray(ShardSearchFailure[]::new), + phaseFailures == null ? PhaseFailure.EMPTY_ARRAY : phaseFailures.toArray(PhaseFailure[]::new), clusters, pointInTimeId ); @@ -338,6 +352,7 @@ public static SearchResponse parseInnerSearchResponse(XContentParser parser) thr String scrollId = null; BytesReference searchContextId = null; List failures = new ArrayList<>(); + List phaseFailures = new ArrayList<>(); SearchResponse.Clusters clusters = SearchResponse.Clusters.EMPTY; for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { if (token == XContentParser.Token.FIELD_NAME) { @@ -400,6 +415,14 @@ public static SearchResponse parseInnerSearchResponse(XContentParser parser) thr } else { parser.skipChildren(); } + } else if (token == XContentParser.Token.START_ARRAY) { + if (SearchResponse.PHASE_FAILURES.match(currentFieldName, parser.getDeprecationHandler())) { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + phaseFailures.add(parsePhaseFailure(parser)); + } + } else { + parser.skipChildren(); + } } } @@ -417,6 +440,7 @@ public static SearchResponse parseInnerSearchResponse(XContentParser parser) thr skippedShards, tookInMillis, failures.toArray(ShardSearchFailure.EMPTY_ARRAY), + phaseFailures.toArray(PhaseFailure.EMPTY_ARRAY), clusters, searchContextId ); @@ -1191,4 +1215,32 @@ public static ShardSearchFailure parseShardSearchFailure(XContentParser parser) } return new ShardSearchFailure(exception, searchShardTarget); } + + public static PhaseFailure parsePhaseFailure(XContentParser parser) throws IOException { + XContentParser.Token token; + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + String currentFieldName = null; + String phase = null; + ElasticsearchException exception = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (PhaseFailure.PHASE_FIELD.equals(currentFieldName)) { + phase = parser.text(); + } else { + parser.skipChildren(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (PhaseFailure.FAILURE_FIELD.equals(currentFieldName)) { + exception = ElasticsearchException.fromXContent(parser); + } else { + parser.skipChildren(); + } + } else { + parser.skipChildren(); + } + } + return new PhaseFailure(phase, exception); + } } diff --git a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java index 11ff403237888..b7dc88acee688 100644 --- a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java +++ b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java @@ -9,6 +9,7 @@ import org.apache.lucene.search.TotalHits; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.search.PhaseFailure; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponse.Clusters; import org.elasticsearch.action.search.SearchResponseMerger; @@ -222,6 +223,7 @@ private SearchResponse buildResponse(long taskStartTimeNanos, InternalAggregatio skippedShards, tookInMillis, buildQueryFailures(), + PhaseFailure.EMPTY_ARRAY, clusters ); } diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java index 98513f611a5d8..9d55a655ce5e4 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java @@ -10,6 +10,7 @@ import org.apache.lucene.index.CorruptIndexException; import org.elasticsearch.TransportVersion; import org.elasticsearch.action.OriginalIndices; +import org.elasticsearch.action.search.PhaseFailure; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.action.support.IndicesOptions; @@ -44,6 +45,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.TimeoutException; import static java.util.Collections.emptyList; import static org.elasticsearch.xpack.core.async.GetAsyncResultRequestTests.randomSearchId; @@ -235,6 +237,7 @@ public void testToXContentWithSearchResponseAfterCompletion() throws IOException 1, took, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); @@ -333,6 +336,147 @@ public void testToXContentWithSearchResponseAfterCompletion() throws IOException } } + // completion_time should be present since search has completed + public void testToXContentWithSearchResponsePhaseFailures() throws IOException { + boolean isRunning = false; + long startTimeMillis = 1689352924517L; + long expirationTimeMillis = 1689784924517L; + long took = 22968L; + long expectedCompletionTime = startTimeMillis + took; + + SearchHits hits = SearchHits.EMPTY_WITHOUT_TOTAL_HITS; + PhaseFailure phaseFailure = new PhaseFailure("reranking", new TimeoutException()); + SearchResponse searchResponse = new SearchResponse( + hits, + null, + null, + false, + null, + null, + 2, + null, + 10, + 9, + 1, + took, + ShardSearchFailure.EMPTY_ARRAY, + new PhaseFailure[] { phaseFailure }, + SearchResponse.Clusters.EMPTY + ); + + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + false, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + } finally { + searchResponse.decRef(); + } + + try { + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : false, + "is_running" : false, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s, + "completion_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "phase_failures" : [ + { + "phase" : "reranking", + "failure" : { + "type" : "timeout_exception", + "reason" : null + } + } + ], + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", startTimeMillis, expirationTimeMillis, expectedCompletionTime, took), Strings.toString(builder)); + } + + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) + .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : false, + "is_running" : false, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s, + "completion_time" : "%s", + "completion_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "phase_failures" : [ + { + "phase" : "reranking", + "failure" : { + "type" : "timeout_exception", + "reason" : null + } + } + ], + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), + startTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), + expirationTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expectedCompletionTime)), + expectedCompletionTime, + took + ), + Strings.toString(builder) + ); + } + } finally { + asyncSearchResponse.decRef(); + } + } + public void testToXContentWithCCSSearchResponseWhileRunning() throws IOException { boolean isRunning = true; long startTimeMillis = 1689352924517L; @@ -357,6 +501,7 @@ public void testToXContentWithCCSSearchResponseWhileRunning() throws IOException 1, took, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, clusters ); AsyncSearchResponse asyncSearchResponse; @@ -590,7 +735,8 @@ public void testToXContentWithCCSSearchResponseAfterCompletion() throws IOExcept 9, 1, took, - new ShardSearchFailure[0], + ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, clusters ); @@ -740,6 +886,7 @@ public void testToXContentWithSearchResponseWhileRunning() throws IOException { 1, took, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); AsyncSearchResponse asyncSearchResponse; diff --git a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichShardMultiSearchAction.java b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichShardMultiSearchAction.java index c273c39d216fc..a0a5294cf83ad 100644 --- a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichShardMultiSearchAction.java +++ b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/action/EnrichShardMultiSearchAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.action.ValidateActions; import org.elasticsearch.action.search.MultiSearchRequest; import org.elasticsearch.action.search.MultiSearchResponse; +import org.elasticsearch.action.search.PhaseFailure; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; @@ -318,6 +319,7 @@ private static SearchResponse createSearchResponse(TopDocs topDocs, SearchHit[] 0, 1L, ShardSearchFailure.EMPTY_ARRAY, + PhaseFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); } finally { diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java index 80b1ff97b725d..1566ddc3a729b 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.action.search.ClosePointInTimeResponse; import org.elasticsearch.action.search.OpenPointInTimeRequest; import org.elasticsearch.action.search.OpenPointInTimeResponse; +import org.elasticsearch.action.search.PhaseFailure; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponse.Clusters; @@ -239,6 +240,7 @@ void handleSearchRequest(ActionListener void handleSearchRequest(ActionListener void handleSearchRequest(ActionListener void handleSearchRequest(ActionListener 0); // at this point the algorithm already started adding up to memory usage @@ -541,27 +513,18 @@ void handleSearchRequest(ActionListener { @@ -1086,7 +1086,7 @@ public void testProfileSearchForApiKeyOwnerWithDomain() throws Exception { return new Authentication.RealmRef(realmName, realmType, "nodeName_" + randomAlphaOfLength(8), realmDomain); }; MultiSearchResponse.Item[] responseItems = new MultiSearchResponse.Item[1]; - responseItems[0] = new MultiSearchResponse.Item(new TestEmptySearchResponse(), null); + responseItems[0] = new MultiSearchResponse.Item(emptySearchResponse(), null); MultiSearchResponse emptyMultiSearchResponse = new MultiSearchResponse(responseItems, randomNonNegativeLong()); try { doAnswer(invocation -> { @@ -1148,7 +1148,7 @@ public void testProfileSearchForOwnerOfMultipleApiKeys() throws Exception { return new Authentication.RealmRef(realmName, realmType, "nodeName"); }; MultiSearchResponse.Item[] responseItems = new MultiSearchResponse.Item[1]; - responseItems[0] = new MultiSearchResponse.Item(new TestEmptySearchResponse(), null); + responseItems[0] = new MultiSearchResponse.Item(emptySearchResponse(), null); MultiSearchResponse emptyMultiSearchResponse = new MultiSearchResponse(responseItems, randomNonNegativeLong()); try { doAnswer(invocation -> { @@ -1199,11 +1199,11 @@ public void testProfileSearchErrorForApiKeyOwner() { MultiSearchResponse.Item[] responseItems = new MultiSearchResponse.Item[2]; // one search request (for one of the key owner) fails if (randomBoolean()) { - responseItems[0] = new MultiSearchResponse.Item(new TestEmptySearchResponse(), null); + responseItems[0] = new MultiSearchResponse.Item(emptySearchResponse(), null); responseItems[1] = new MultiSearchResponse.Item(null, new Exception("test search failure")); } else { responseItems[0] = new MultiSearchResponse.Item(null, new Exception("test search failure")); - responseItems[1] = new MultiSearchResponse.Item(new TestEmptySearchResponse(), null); + responseItems[1] = new MultiSearchResponse.Item(emptySearchResponse(), null); } MultiSearchResponse multiSearchResponseWithError = new MultiSearchResponse(responseItems, randomNonNegativeLong()); try { @@ -1418,26 +1418,7 @@ private static ApiKey createApiKeyForOwner(String apiKeyId, String username, Str ); } - private static class TestEmptySearchResponse extends SearchResponse { - - TestEmptySearchResponse() { - super( - SearchHits.EMPTY_WITH_TOTAL_HITS, - null, - null, - false, - null, - null, - 1, - null, - 0, - 0, - 0, - 0L, - ShardSearchFailure.EMPTY_ARRAY, - Clusters.EMPTY, - null - ); - } + private static SearchResponse emptySearchResponse() { + return SearchResponseUtils.response().searchHits(SearchHits.EMPTY_WITH_TOTAL_HITS).numReducePhases(1).build(); } } diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/ClientTransformIndexerTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/ClientTransformIndexerTests.java index c1d36c329de4d..29779f5302ec3 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/ClientTransformIndexerTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/ClientTransformIndexerTests.java @@ -19,7 +19,6 @@ import org.elasticsearch.action.search.OpenPointInTimeResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.action.support.ActionTestUtils; import org.elasticsearch.client.internal.ParentTaskAssigningClient; import org.elasticsearch.cluster.ClusterState; @@ -39,11 +38,10 @@ import org.elasticsearch.search.SearchContextMissingException; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.SearchResponseUtils; import org.elasticsearch.search.builder.PointInTimeBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.internal.ShardSearchContextId; -import org.elasticsearch.search.profile.SearchProfileResults; -import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.client.NoOpClient; @@ -640,31 +638,20 @@ && new BytesArray("the_pit_id+++").equals(searchRequest.pointInTimeBuilder().get } else { ActionListener.respondAndRelease( listener, - (Response) new SearchResponse( + (Response) SearchResponseUtils.response( SearchHits.unpooled( new SearchHit[] { SearchHit.unpooled(1) }, new TotalHits(1L, TotalHits.Relation.EQUAL_TO), 1.0f - ), - // Simulate completely null aggs - null, - new Suggest(Collections.emptyList()), - false, - false, - new SearchProfileResults(Collections.emptyMap()), - 1, - null, - 1, - 1, - 0, - 0, - ShardSearchFailure.EMPTY_ARRAY, - SearchResponse.Clusters.EMPTY, - // copy the pit from the request - searchRequest.pointInTimeBuilder() != null - ? CompositeBytesReference.of(searchRequest.pointInTimeBuilder().getEncodedId(), new BytesArray("+")) - : null + ) ) + // copy the pit from the request + .pointInTimeId( + searchRequest.pointInTimeBuilder() != null + ? CompositeBytesReference.of(searchRequest.pointInTimeBuilder().getEncodedId(), new BytesArray("+")) + : null + ) + .build() ); } diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java index 7498a51321d55..2b5925aebb76b 100644 --- a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java +++ b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java @@ -165,6 +165,7 @@ public RestResponse buildResponse(SearchResponse searchResponse) throws Exceptio searchResponse.getSkippedShards(), searchResponse.getTook().millis(), searchResponse.getShardFailures(), + searchResponse.getPhaseFailures(), searchResponse.getClusters() ); try {