Skip to content

Commit ed4eecc

Browse files
committed
Pre-sort shards based on the max/min value of the primary sort field (#49092)
This change automatically pre-sort search shards on search requests that use a primary sort based on the value of a field. When possible, the can_match phase will extract the min/max (depending on the provided sort order) values of each shard and use it to pre-sort the shards prior to running the subsequent phases. This feature can be useful to ensure that shards that contain recent data are executed first so that intermediate merge have more chance to contain contiguous data (think of date_histogram for instance) but it could also be used in a follow up to early terminate sorted top-hits queries that don't require the total hit count. The latter could significantly speed up the retrieval of the most/least recent documents from time-based indices. Relates #49091
1 parent c13fce6 commit ed4eecc

File tree

15 files changed

+616
-50
lines changed

15 files changed

+616
-50
lines changed

server/src/main/java/org/elasticsearch/action/search/AbstractSearchAsyncAction.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ abstract class AbstractSearchAsyncAction<Result extends SearchPhaseResult> exten
113113
iterators.add(iterator);
114114
}
115115
}
116-
this.toSkipShardsIts = new GroupShardsIterator<>(toSkipIterators);
117-
this.shardsIts = new GroupShardsIterator<>(iterators);
116+
this.toSkipShardsIts = new GroupShardsIterator<>(toSkipIterators, false);
117+
this.shardsIts = new GroupShardsIterator<>(iterators, false);
118118
// we need to add 1 for non active partition, since we count it in the total. This means for each shard in the iterator we sum up
119119
// it's number of active shards but use 1 as the default if no replica of a shard is active at this point.
120120
// on a per shards level we use shardIt.remaining() to increment the totalOps pointer but add 1 for the current shard result

server/src/main/java/org/elasticsearch/action/search/CanMatchPreFilterSearchPhase.java

+63-18
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,25 @@
2323
import org.elasticsearch.action.ActionListener;
2424
import org.elasticsearch.cluster.routing.GroupShardsIterator;
2525
import org.elasticsearch.cluster.routing.ShardRouting;
26-
import org.elasticsearch.search.SearchService;
26+
import org.elasticsearch.search.SearchService.CanMatchResponse;
27+
import org.elasticsearch.search.builder.SearchSourceBuilder;
2728
import org.elasticsearch.search.internal.AliasFilter;
29+
import org.elasticsearch.search.sort.FieldSortBuilder;
30+
import org.elasticsearch.search.sort.MinAndMax;
31+
import org.elasticsearch.search.sort.SortOrder;
2832
import org.elasticsearch.transport.Transport;
2933

34+
import java.util.Arrays;
35+
import java.util.Comparator;
36+
import java.util.List;
3037
import java.util.Map;
38+
import java.util.Objects;
3139
import java.util.Set;
3240
import java.util.concurrent.Executor;
3341
import java.util.function.BiFunction;
3442
import java.util.function.Function;
43+
import java.util.stream.Collectors;
44+
import java.util.stream.IntStream;
3545
import java.util.stream.Stream;
3646

3747
/**
@@ -40,8 +50,12 @@
4050
* from the search. The extra round trip to the search shards is very cheap and is not subject to rejections
4151
* which allows to fan out to more shards at the same time without running into rejections even if we are hitting a
4252
* large portion of the clusters indices.
53+
* This phase can also be used to pre-sort shards based on min/max values in each shard of the provided primary sort.
54+
* When the query primary sort is perform on a field, this phase extracts the min/max value in each shard and
55+
* sort them according to the provided order. This can be useful for instance to ensure that shards that contain recent
56+
* data are executed first when sorting by descending timestamp.
4357
*/
44-
final class CanMatchPreFilterSearchPhase extends AbstractSearchAsyncAction<SearchService.CanMatchResponse> {
58+
final class CanMatchPreFilterSearchPhase extends AbstractSearchAsyncAction<CanMatchResponse> {
4559

4660
private final Function<GroupShardsIterator<SearchShardIterator>, SearchPhase> phaseFactory;
4761
private final GroupShardsIterator<SearchShardIterator> shardsIts;
@@ -58,26 +72,26 @@ final class CanMatchPreFilterSearchPhase extends AbstractSearchAsyncAction<Searc
5872
//We set max concurrent shard requests to the number of shards so no throttling happens for can_match requests
5973
super("can_match", logger, searchTransportService, nodeIdToConnection, aliasFilter, concreteIndexBoosts, indexRoutings,
6074
executor, request, listener, shardsIts, timeProvider, clusterStateVersion, task,
61-
new BitSetSearchPhaseResults(shardsIts.size()), shardsIts.size(), clusters);
75+
new CanMatchSearchPhaseResults(shardsIts.size()), shardsIts.size(), clusters);
6276
this.phaseFactory = phaseFactory;
6377
this.shardsIts = shardsIts;
6478
}
6579

6680
@Override
6781
protected void executePhaseOnShard(SearchShardIterator shardIt, ShardRouting shard,
68-
SearchActionListener<SearchService.CanMatchResponse> listener) {
82+
SearchActionListener<CanMatchResponse> listener) {
6983
getSearchTransport().sendCanMatch(getConnection(shardIt.getClusterAlias(), shard.currentNodeId()),
7084
buildShardSearchRequest(shardIt), getTask(), listener);
7185
}
7286

7387
@Override
74-
protected SearchPhase getNextPhase(SearchPhaseResults<SearchService.CanMatchResponse> results,
88+
protected SearchPhase getNextPhase(SearchPhaseResults<CanMatchResponse> results,
7589
SearchPhaseContext context) {
7690

77-
return phaseFactory.apply(getIterator((BitSetSearchPhaseResults) results, shardsIts));
91+
return phaseFactory.apply(getIterator((CanMatchSearchPhaseResults) results, shardsIts));
7892
}
7993

80-
private GroupShardsIterator<SearchShardIterator> getIterator(BitSetSearchPhaseResults results,
94+
private GroupShardsIterator<SearchShardIterator> getIterator(CanMatchSearchPhaseResults results,
8195
GroupShardsIterator<SearchShardIterator> shardsIts) {
8296
int cardinality = results.getNumPossibleMatches();
8397
FixedBitSet possibleMatches = results.getPossibleMatches();
@@ -86,6 +100,7 @@ private GroupShardsIterator<SearchShardIterator> getIterator(BitSetSearchPhaseRe
86100
// to produce a valid search result with all the aggs etc.
87101
possibleMatches.set(0);
88102
}
103+
SearchSourceBuilder source = getRequest().source();
89104
int i = 0;
90105
for (SearchShardIterator iter : shardsIts) {
91106
if (possibleMatches.get(i++)) {
@@ -94,24 +109,48 @@ private GroupShardsIterator<SearchShardIterator> getIterator(BitSetSearchPhaseRe
94109
iter.resetAndSkip();
95110
}
96111
}
97-
return shardsIts;
112+
if (shouldSortShards(results.minAndMaxes) == false) {
113+
return shardsIts;
114+
}
115+
FieldSortBuilder fieldSort = FieldSortBuilder.getPrimaryFieldSortOrNull(source);
116+
return new GroupShardsIterator<>(sortShards(shardsIts, results.minAndMaxes, fieldSort.order()), false);
98117
}
99118

100-
private static final class BitSetSearchPhaseResults extends SearchPhaseResults<SearchService.CanMatchResponse> {
119+
private static List<SearchShardIterator> sortShards(GroupShardsIterator<SearchShardIterator> shardsIts,
120+
MinAndMax<?>[] minAndMaxes,
121+
SortOrder order) {
122+
return IntStream.range(0, shardsIts.size())
123+
.boxed()
124+
.sorted(shardComparator(shardsIts, minAndMaxes, order))
125+
.map(ord -> shardsIts.get(ord))
126+
.collect(Collectors.toList());
127+
}
101128

129+
private static boolean shouldSortShards(MinAndMax<?>[] minAndMaxes) {
130+
return Arrays.stream(minAndMaxes).anyMatch(Objects::nonNull);
131+
}
132+
133+
private static Comparator<Integer> shardComparator(GroupShardsIterator<SearchShardIterator> shardsIts,
134+
MinAndMax<?>[] minAndMaxes,
135+
SortOrder order) {
136+
final Comparator<Integer> comparator = Comparator.comparing(index -> minAndMaxes[index], MinAndMax.getComparator(order));
137+
return comparator.thenComparing(index -> shardsIts.get(index).shardId());
138+
}
139+
140+
private static final class CanMatchSearchPhaseResults extends SearchPhaseResults<CanMatchResponse> {
102141
private final FixedBitSet possibleMatches;
142+
private final MinAndMax<?>[] minAndMaxes;
103143
private int numPossibleMatches;
104144

105-
BitSetSearchPhaseResults(int size) {
145+
CanMatchSearchPhaseResults(int size) {
106146
super(size);
107147
possibleMatches = new FixedBitSet(size);
148+
minAndMaxes = new MinAndMax[size];
108149
}
109150

110151
@Override
111-
void consumeResult(SearchService.CanMatchResponse result) {
112-
if (result.canMatch()) {
113-
consumeShardFailure(result.getShardIndex());
114-
}
152+
void consumeResult(CanMatchResponse result) {
153+
consumeResult(result.getShardIndex(), result.canMatch(), result.minAndMax());
115154
}
116155

117156
@Override
@@ -120,12 +159,18 @@ boolean hasResult(int shardIndex) {
120159
}
121160

122161
@Override
123-
synchronized void consumeShardFailure(int shardIndex) {
162+
void consumeShardFailure(int shardIndex) {
124163
// we have to carry over shard failures in order to account for them in the response.
125-
possibleMatches.set(shardIndex);
126-
numPossibleMatches++;
164+
consumeResult(shardIndex, true, null);
127165
}
128166

167+
synchronized void consumeResult(int shardIndex, boolean canMatch, MinAndMax<?> minAndMax) {
168+
if (canMatch) {
169+
possibleMatches.set(shardIndex);
170+
numPossibleMatches++;
171+
}
172+
minAndMaxes[shardIndex] = minAndMax;
173+
}
129174

130175
synchronized int getNumPossibleMatches() {
131176
return numPossibleMatches;
@@ -136,7 +181,7 @@ synchronized FixedBitSet getPossibleMatches() {
136181
}
137182

138183
@Override
139-
Stream<SearchService.CanMatchResponse> getSuccessfulResults() {
184+
Stream<CanMatchResponse> getSuccessfulResults() {
140185
return Stream.empty();
141186
}
142187
}

server/src/main/java/org/elasticsearch/action/search/SearchActionListener.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*/
2929
abstract class SearchActionListener<T extends SearchPhaseResult> implements ActionListener<T> {
3030

31-
private final int requestIndex;
31+
final int requestIndex;
3232
private final SearchShardTarget searchShardTarget;
3333

3434
protected SearchActionListener(SearchShardTarget searchShardTarget,

server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.elasticsearch.search.internal.SearchContext;
5757
import org.elasticsearch.search.profile.ProfileShardResult;
5858
import org.elasticsearch.search.profile.SearchProfileShardResults;
59+
import org.elasticsearch.search.sort.FieldSortBuilder;
5960
import org.elasticsearch.tasks.Task;
6061
import org.elasticsearch.threadpool.ThreadPool;
6162
import org.elasticsearch.transport.RemoteClusterAware;
@@ -615,9 +616,9 @@ static BiFunction<String, String, Transport.Connection> buildConnectionLookup(St
615616
private static boolean shouldPreFilterSearchShards(SearchRequest searchRequest,
616617
GroupShardsIterator<SearchShardIterator> shardIterators) {
617618
SearchSourceBuilder source = searchRequest.source();
618-
return searchRequest.searchType() == QUERY_THEN_FETCH && // we can't do this for DFS it needs to fan out to all shards all the time
619-
SearchService.canRewriteToMatchNone(source) &&
620-
searchRequest.getPreFilterShardSize() < shardIterators.size();
619+
return searchRequest.searchType() == QUERY_THEN_FETCH // we can't do this for DFS it needs to fan out to all shards all the time
620+
&& (SearchService.canRewriteToMatchNone(source) || FieldSortBuilder.hasPrimaryFieldSort(source))
621+
&& searchRequest.getPreFilterShardSize() < shardIterators.size();
621622
}
622623

623624
static GroupShardsIterator<SearchShardIterator> mergeShardsIterators(GroupShardsIterator<ShardIterator> localShardsIterator,

server/src/main/java/org/elasticsearch/cluster/routing/GroupShardsIterator.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,16 @@ public final class GroupShardsIterator<ShardIt extends ShardIterator> implements
3838
* Constructs a enw GroupShardsIterator from the given list.
3939
*/
4040
public GroupShardsIterator(List<ShardIt> iterators) {
41-
CollectionUtil.timSort(iterators);
41+
this(iterators, true);
42+
}
43+
44+
/**
45+
* Constructs a new GroupShardsIterator from the given list.
46+
*/
47+
public GroupShardsIterator(List<ShardIt> iterators, boolean useSort) {
48+
if (useSort) {
49+
CollectionUtil.timSort(iterators);
50+
}
4251
this.iterators = iterators;
4352
}
4453

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public static final class DateFieldType extends MappedFieldType {
252252
protected DateMathParser dateMathParser;
253253
protected Resolution resolution;
254254

255-
DateFieldType() {
255+
public DateFieldType() {
256256
super();
257257
setTokenized(false);
258258
setHasDocValues(true);

server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ public final String typeName() {
816816
return name;
817817
}
818818
/** Get the associated numeric type */
819-
final NumericType numericType() {
819+
public final NumericType numericType() {
820820
return numericType;
821821
}
822822
public abstract Query termQuery(String field, Object value);
@@ -909,6 +909,10 @@ public String typeName() {
909909
return type.name;
910910
}
911911

912+
public NumericType numericType() {
913+
return type.numericType();
914+
}
915+
912916
@Override
913917
public Query existsQuery(QueryShardContext context) {
914918
if (hasDocValues()) {

server/src/main/java/org/elasticsearch/search/SearchService.java

+26-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.lucene.search.FieldDoc;
2525
import org.apache.lucene.search.TopDocs;
2626
import org.elasticsearch.ElasticsearchException;
27+
import org.elasticsearch.Version;
2728
import org.elasticsearch.action.ActionListener;
2829
import org.elasticsearch.action.ActionRunnable;
2930
import org.elasticsearch.action.OriginalIndices;
@@ -92,6 +93,8 @@
9293
import org.elasticsearch.search.query.ScrollQuerySearchResult;
9394
import org.elasticsearch.search.rescore.RescorerBuilder;
9495
import org.elasticsearch.search.searchafter.SearchAfterBuilder;
96+
import org.elasticsearch.search.sort.FieldSortBuilder;
97+
import org.elasticsearch.search.sort.MinAndMax;
9598
import org.elasticsearch.search.sort.SortAndFormats;
9699
import org.elasticsearch.search.sort.SortBuilder;
97100
import org.elasticsearch.search.suggest.Suggest;
@@ -1013,7 +1016,7 @@ public AliasFilter buildAliasFilter(ClusterState state, String index, Set<String
10131016
* This method can have false positives while if it returns <code>false</code> the query won't match any documents on the current
10141017
* shard.
10151018
*/
1016-
public boolean canMatch(ShardSearchRequest request) throws IOException {
1019+
public CanMatchResponse canMatch(ShardSearchRequest request) throws IOException {
10171020
assert request.searchType() == SearchType.QUERY_THEN_FETCH : "unexpected search type: " + request.searchType();
10181021
IndexService indexService = indicesService.indexServiceSafe(request.shardId().getIndex());
10191022
IndexShard indexShard = indexService.getShard(request.shardId().getId());
@@ -1023,18 +1026,20 @@ public boolean canMatch(ShardSearchRequest request) throws IOException {
10231026
QueryShardContext context = indexService.newQueryShardContext(request.shardId().id(), searcher,
10241027
request::nowInMillis, request.getClusterAlias());
10251028
Rewriteable.rewrite(request.getRewriteable(), context, false);
1029+
FieldSortBuilder sortBuilder = FieldSortBuilder.getPrimaryFieldSortOrNull(request.source());
1030+
MinAndMax<?> minMax = sortBuilder != null ? FieldSortBuilder.getMinMaxOrNull(context, sortBuilder) : null;
10261031
if (canRewriteToMatchNone(request.source())) {
10271032
QueryBuilder queryBuilder = request.source().query();
1028-
return queryBuilder instanceof MatchNoneQueryBuilder == false;
1033+
return new CanMatchResponse(queryBuilder instanceof MatchNoneQueryBuilder == false, minMax);
10291034
}
1030-
return true; // null query means match_all
1035+
// null query means match_all
1036+
return new CanMatchResponse(true, minMax);
10311037
}
10321038
}
10331039

1034-
10351040
public void canMatch(ShardSearchRequest request, ActionListener<CanMatchResponse> listener) {
10361041
try {
1037-
listener.onResponse(new CanMatchResponse(canMatch(request)));
1042+
listener.onResponse(canMatch(request));
10381043
} catch (IOException e) {
10391044
listener.onFailure(e);
10401045
}
@@ -1053,6 +1058,7 @@ public static boolean canRewriteToMatchNone(SearchSourceBuilder source) {
10531058
return aggregations == null || aggregations.mustVisitAllDocs() == false;
10541059
}
10551060

1061+
10561062
/*
10571063
* Rewrites the search request with a light weight rewrite context in order to fetch resources asynchronously
10581064
* The action listener is guaranteed to be executed on the search thread-pool
@@ -1088,24 +1094,38 @@ public InternalAggregation.ReduceContext createReduceContext(boolean finalReduce
10881094

10891095
public static final class CanMatchResponse extends SearchPhaseResult {
10901096
private final boolean canMatch;
1097+
private final MinAndMax<?> minAndMax;
10911098

10921099
public CanMatchResponse(StreamInput in) throws IOException {
10931100
super(in);
10941101
this.canMatch = in.readBoolean();
1102+
if (in.getVersion().onOrAfter(Version.V_7_6_0)) {
1103+
minAndMax = in.readOptionalWriteable(MinAndMax::new);
1104+
} else {
1105+
minAndMax = null;
1106+
}
10951107
}
10961108

1097-
public CanMatchResponse(boolean canMatch) {
1109+
public CanMatchResponse(boolean canMatch, MinAndMax<?> minAndMax) {
10981110
this.canMatch = canMatch;
1111+
this.minAndMax = minAndMax;
10991112
}
11001113

11011114
@Override
11021115
public void writeTo(StreamOutput out) throws IOException {
11031116
out.writeBoolean(canMatch);
1117+
if (out.getVersion().onOrAfter(Version.V_7_6_0)) {
1118+
out.writeOptionalWriteable(minAndMax);
1119+
}
11041120
}
11051121

11061122
public boolean canMatch() {
11071123
return canMatch;
11081124
}
1125+
1126+
public MinAndMax<?> minAndMax() {
1127+
return minAndMax;
1128+
}
11091129
}
11101130

11111131
/**

0 commit comments

Comments
 (0)