diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/310_sequence_numbers_disabled.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/310_sequence_numbers_disabled.yml index 03f34e0ab2b1b..cde52c5e1eb02 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/310_sequence_numbers_disabled.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/310_sequence_numbers_disabled.yml @@ -85,13 +85,13 @@ setup: - match: { items.1.index._primary_term: 0 } --- -"requesting seq_no_primary_term is rejected": +"seq_no_primary_term returns sentinel values": - do: - catch: bad_request search: index: test body: seq_no_primary_term: true - - match: { error.root_cause.0.type: "illegal_argument_exception" } - - match: { error.root_cause.0.reason: "Cannot request seq_no_primary_term on index [test] because [index.disable_sequence_numbers] is [true]" } + - match: { hits.total.value: 1 } + - match: { hits.hits.0._seq_no: -2 } + - match: { hits.hits.0._primary_term: 0 } diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index e07890158a597..59d5b5ca8f789 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -27,6 +27,7 @@ import org.elasticsearch.rest.action.RestActions; import org.elasticsearch.rest.action.RestCancellableNodeClient; import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; +import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchService; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.crossproject.CrossProjectModeDecider; @@ -37,12 +38,14 @@ import org.elasticsearch.search.suggest.SuggestBuilder; import org.elasticsearch.search.suggest.term.TermSuggestionBuilder; import org.elasticsearch.usage.SearchUsageHolder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.IntConsumer; @@ -139,7 +142,8 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC return channel -> { RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel()); - cancelClient.execute(TransportSearchAction.TYPE, searchRequest, new RestRefCountedChunkedToXContentListener<>(channel)); + var params = serializationParams(searchRequest, channel.request()); + cancelClient.execute(TransportSearchAction.TYPE, searchRequest, new RestRefCountedChunkedToXContentListener<>(channel, params)); }; } @@ -474,6 +478,15 @@ private static void checkSearchType(RestRequest restRequest, SearchRequest searc } } + private static ToXContent.Params serializationParams(SearchRequest searchRequest, ToXContent.Params channelParams) { + if (searchRequest.source() != null + && searchRequest.source().seqNoAndPrimaryTerm() != null + && searchRequest.source().seqNoAndPrimaryTerm()) { + return new ToXContent.DelegatingMapParams(Map.of(SearchHit.SEQ_NO_PRIMARY_TERM_PARAMS_KEY, "true"), channelParams); + } + return channelParams; + } + @Override protected Set responseParams() { return RESPONSE_PARAMS; diff --git a/server/src/main/java/org/elasticsearch/search/SearchHit.java b/server/src/main/java/org/elasticsearch/search/SearchHit.java index 4f4c3868e5415..dba1e4eacd545 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchHit.java +++ b/server/src/main/java/org/elasticsearch/search/SearchHit.java @@ -73,6 +73,12 @@ public final class SearchHit implements Writeable, ToXContentObject, RefCounted static final float DEFAULT_SCORE = Float.NaN; private float score; + /** + * ToXContent param key that, when set to {@code true}, causes {@code _seq_no} and {@code _primary_term} to be + * emitted even when they hold sentinel (unassigned) values. Used for indices with sequence numbers disabled. + */ + public static final String SEQ_NO_PRIMARY_TERM_PARAMS_KEY = "seq_no_primary_term"; + static final int NO_RANK = -1; private int rank; @@ -832,7 +838,7 @@ public XContentBuilder toInnerXContent(XContentBuilder builder, Params params) t builder.field(Fields._VERSION, version); } - if (seqNo != SequenceNumbers.UNASSIGNED_SEQ_NO) { + if (seqNo != SequenceNumbers.UNASSIGNED_SEQ_NO || params.paramAsBoolean(SEQ_NO_PRIMARY_TERM_PARAMS_KEY, false)) { builder.field(Fields._SEQ_NO, seqNo); builder.field(Fields._PRIMARY_TERM, primaryTerm); } diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index 462359f5f1a37..1e21a39baff53 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -1870,13 +1870,6 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc } if (source.seqNoAndPrimaryTerm() != null) { - if (source.seqNoAndPrimaryTerm() && context.getSearchExecutionContext().getIndexSettings().sequenceNumbersDisabled()) { - throw new IllegalArgumentException( - "Cannot request seq_no_primary_term on index [" - + context.getSearchExecutionContext().index().getName() - + "] because [index.disable_sequence_numbers] is [true]" - ); - } context.seqNoAndPrimaryTerm(source.seqNoAndPrimaryTerm()); } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/SeqNoPrimaryTermPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/SeqNoPrimaryTermPhase.java index 3007aa8c08089..cf118216e24e3 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/SeqNoPrimaryTermPhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/SeqNoPrimaryTermPhase.java @@ -20,11 +20,31 @@ import java.io.IOException; public final class SeqNoPrimaryTermPhase implements FetchSubPhase { + + private static final FetchSubPhaseProcessor UNASSIGNED_PROCESSOR = new FetchSubPhaseProcessor() { + @Override + public void setNextReader(LeafReaderContext readerContext) {} + + @Override + public StoredFieldsSpec storedFieldsSpec() { + return StoredFieldsSpec.NO_REQUIREMENTS; + } + + @Override + public void process(HitContext hitContext) { + hitContext.hit().setSeqNo(SequenceNumbers.UNASSIGNED_SEQ_NO); + hitContext.hit().setPrimaryTerm(SequenceNumbers.UNASSIGNED_PRIMARY_TERM); + } + }; + @Override public FetchSubPhaseProcessor getProcessor(FetchContext context) { if (context.seqNoAndPrimaryTerm() == false) { return null; } + if (context.getSearchExecutionContext().getIndexSettings().sequenceNumbersDisabled()) { + return UNASSIGNED_PROCESSOR; + } return new FetchSubPhaseProcessor() { NumericDocValues seqNoField = null; diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java index 349c6896b7b58..cc31052327a86 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceSingleNodeTests.java @@ -72,6 +72,7 @@ import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.search.stats.SearchStats; +import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.SearchOperationListener; import org.elasticsearch.index.shard.ShardId; @@ -166,6 +167,7 @@ import static org.elasticsearch.search.SearchService.SEARCH_WORKER_THREADS_ENABLED; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailuresAndResponse; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; @@ -2898,7 +2900,7 @@ public void testSlicingBehaviourForParallelCollection() throws Exception { } } - public void testSeqNoAndPrimaryTermRejectedWhenSequenceNumbersDisabled() throws IOException { + public void testSeqNoAndPrimaryTermReturnsSentinelsWhenSequenceNumbersDisabled() { assumeTrue("Test should only run with feature flag", IndexSettings.DISABLE_SEQUENCE_NUMBERS_FEATURE_FLAG); final Settings settings = Settings.builder() .put(IndexSettings.DISABLE_SEQUENCE_NUMBERS.getKey(), true) @@ -2907,37 +2909,11 @@ public void testSeqNoAndPrimaryTermRejectedWhenSequenceNumbersDisabled() throws createIndex("test-no-seqno", settings); prepareIndex("test-no-seqno").setId("1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get(); - final SearchService service = getInstanceFromNode(SearchService.class); - final IndicesService indicesService = getInstanceFromNode(IndicesService.class); - final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("test-no-seqno")); - final IndexShard indexShard = indexService.getShard(0); - - SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(true); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.seqNoAndPrimaryTerm(true); - searchRequest.source(searchSourceBuilder); - - final ShardSearchRequest request = new ShardSearchRequest( - OriginalIndices.NONE, - searchRequest, - indexShard.shardId(), - 0, - 1, - AliasFilter.EMPTY, - 1.0f, - -1, - null - ); - try (ReaderContext reader = createReaderContext(indexService, indexShard)) { - IllegalArgumentException ex = expectThrows( - IllegalArgumentException.class, - () -> service.createContext(reader, request, mock(SearchShardTask.class), ResultsType.NONE, randomBoolean()) - ); - assertEquals( - "Cannot request seq_no_primary_term on index [test-no-seqno] because [index.disable_sequence_numbers] is [true]", - ex.getMessage() - ); - } + assertNoFailuresAndResponse(client().prepareSearch("test-no-seqno").seqNoAndPrimaryTerm(true), response -> { + assertHitCount(response, 1); + assertEquals(SequenceNumbers.UNASSIGNED_SEQ_NO, response.getHits().getAt(0).getSeqNo()); + assertEquals(SequenceNumbers.UNASSIGNED_PRIMARY_TERM, response.getHits().getAt(0).getPrimaryTerm()); + }); } private static ReaderContext createReaderContext(IndexService indexService, IndexShard indexShard) {