Skip to content

Commit 97c0681

Browse files
authored
Fix an optimization in terms agg (backport #57438) (#57547)
When the `terms` agg runs against strings and uses global ordinals it has an optimization when it collects segments that only ever have a single value for the particular string. This is *very* common. But I broke it in #57241. This fixes that optimization and adds `debug` information that you can use to see how often we collect segments of each type. And adds a test to make sure that I don't break the optimization again. We also had a specialiation for when there isn't a filter on the terms to aggregate. I had removed that specialization in #57241 which resulted in some slow down as well. This adds it back but in a more clear way. And, hopefully, a way that is marginally faster when there *is* a filter. Closes #57407
1 parent e50f514 commit 97c0681

File tree

2 files changed

+151
-68
lines changed

2 files changed

+151
-68
lines changed

rest-api-spec/src/main/resources/rest-api-spec/test/search.aggregation/20_terms.yml

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ setup:
779779
body: { "size" : 0, "aggs" : { "no_field_terms" : { "terms" : { "size": 1 } } } }
780780

781781
---
782-
"profiler":
782+
"global ords profiler":
783783
- skip:
784784
version: " - 7.8.99"
785785
reason: debug information added in 7.9.0
@@ -808,6 +808,7 @@ setup:
808808
terms:
809809
field: str
810810
collect_mode: breadth_first
811+
execution_hint: global_ordinals
811812
aggs:
812813
max_number:
813814
max:
@@ -823,9 +824,83 @@ setup:
823824
- match: { profile.shards.0.aggregations.0.breakdown.collect_count: 4 }
824825
- match: { profile.shards.0.aggregations.0.debug.deferred_aggregators: [ max_number ] }
825826
- match: { profile.shards.0.aggregations.0.debug.collection_strategy: dense }
827+
- match: { profile.shards.0.aggregations.0.debug.result_strategy: terms }
828+
- gt: { profile.shards.0.aggregations.0.debug.segments_with_single_valued_ords: 0 }
829+
- match: { profile.shards.0.aggregations.0.debug.segments_with_multi_valued_ords: 0 }
830+
- match: { profile.shards.0.aggregations.0.debug.has_filter: false }
826831
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
827832
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
828833

834+
- do:
835+
indices.create:
836+
index: test_3
837+
body:
838+
settings:
839+
number_of_shards: 1
840+
number_of_replicas: 0
841+
mappings:
842+
properties:
843+
str:
844+
type: keyword
845+
- do:
846+
bulk:
847+
index: test_3
848+
refresh: true
849+
body: |
850+
{ "index": {} }
851+
{ "str": ["pig", "sheep"], "number": 100 }
852+
853+
- do:
854+
search:
855+
index: test_3
856+
body:
857+
profile: true
858+
size: 0
859+
aggs:
860+
str_terms:
861+
terms:
862+
field: str
863+
collect_mode: breadth_first
864+
execution_hint: global_ordinals
865+
aggs:
866+
max_number:
867+
max:
868+
field: number
869+
- match: { aggregations.str_terms.buckets.0.key: pig }
870+
- match: { aggregations.str_terms.buckets.0.max_number.value: 100 }
871+
- match: { aggregations.str_terms.buckets.1.key: sheep }
872+
- match: { aggregations.str_terms.buckets.1.max_number.value: 100 }
873+
- match: { profile.shards.0.aggregations.0.type: GlobalOrdinalsStringTermsAggregator }
874+
- match: { profile.shards.0.aggregations.0.description: str_terms }
875+
- match: { profile.shards.0.aggregations.0.breakdown.collect_count: 1 }
876+
- match: { profile.shards.0.aggregations.0.debug.deferred_aggregators: [ max_number ] }
877+
- match: { profile.shards.0.aggregations.0.debug.collection_strategy: dense }
878+
- match: { profile.shards.0.aggregations.0.debug.result_strategy: terms }
879+
- match: { profile.shards.0.aggregations.0.debug.segments_with_single_valued_ords: 0 }
880+
- gt: { profile.shards.0.aggregations.0.debug.segments_with_multi_valued_ords: 0 }
881+
- match: { profile.shards.0.aggregations.0.debug.has_filter: false }
882+
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
883+
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
884+
885+
---
886+
"numeric profiler":
887+
- skip:
888+
version: " - 7.8.99"
889+
reason: debug information added in 7.9.0
890+
- do:
891+
bulk:
892+
index: test_1
893+
refresh: true
894+
body: |
895+
{ "index": {} }
896+
{ "number": 1 }
897+
{ "index": {} }
898+
{ "number": 3 }
899+
{ "index": {} }
900+
{ "number": 1 }
901+
{ "index": {} }
902+
{ "number": 1 }
903+
829904
- do:
830905
search:
831906
index: test_1

server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/GlobalOrdinalsStringTermsAggregator.java

Lines changed: 75 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.elasticsearch.common.util.IntArray;
3434
import org.elasticsearch.common.util.LongHash;
3535
import org.elasticsearch.common.xcontent.XContentBuilder;
36-
import org.elasticsearch.index.fielddata.AbstractSortedSetDocValues;
3736
import org.elasticsearch.search.DocValueFormat;
3837
import org.elasticsearch.search.aggregations.Aggregator;
3938
import org.elasticsearch.search.aggregations.AggregatorFactories;
@@ -73,6 +72,8 @@ public class GlobalOrdinalsStringTermsAggregator extends AbstractStringTermsAggr
7372
private final long valueCount;
7473
private final GlobalOrdLookupFunction lookupGlobalOrd;
7574
protected final CollectionStrategy collectionStrategy;
75+
protected int segmentsWithSingleValuedOrds = 0;
76+
protected int segmentsWithMultiValuedOrds = 0;
7677

7778
public interface GlobalOrdLookupFunction {
7879
BytesRef apply(long ord) throws IOException;
@@ -102,32 +103,68 @@ public GlobalOrdinalsStringTermsAggregator(
102103
valuesSource.globalOrdinalsValues(context.searcher().getIndexReader().leaves().get(0)) : DocValues.emptySortedSet();
103104
this.valueCount = values.getValueCount();
104105
this.lookupGlobalOrd = values::lookupOrd;
105-
this.acceptedGlobalOrdinals = includeExclude == null ? l -> true : includeExclude.acceptedGlobalOrdinals(values)::get;
106+
this.acceptedGlobalOrdinals = includeExclude == null ? ALWAYS_TRUE : includeExclude.acceptedGlobalOrdinals(values)::get;
106107
this.collectionStrategy = remapGlobalOrds ? new RemapGlobalOrds() : new DenseGlobalOrds();
107108
}
108109

109110
String descriptCollectionStrategy() {
110111
return collectionStrategy.describe();
111112
}
112113

113-
private SortedSetDocValues getGlobalOrds(LeafReaderContext ctx) throws IOException {
114-
return acceptedGlobalOrdinals == null ?
115-
valuesSource.globalOrdinalsValues(ctx) : new FilteredOrdinals(valuesSource.globalOrdinalsValues(ctx), acceptedGlobalOrdinals);
116-
}
117-
118114
@Override
119115
public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, LeafBucketCollector sub) throws IOException {
120-
SortedSetDocValues globalOrds = getGlobalOrds(ctx);
116+
SortedSetDocValues globalOrds = valuesSource.globalOrdinalsValues(ctx);
121117
collectionStrategy.globalOrdsReady(globalOrds);
122118
SortedDocValues singleValues = DocValues.unwrapSingleton(globalOrds);
123119
if (singleValues != null) {
120+
segmentsWithSingleValuedOrds++;
121+
if (acceptedGlobalOrdinals == ALWAYS_TRUE) {
122+
/*
123+
* Optimize when there isn't a filter because that is very
124+
* common and marginally faster.
125+
*/
126+
return resultStrategy.wrapCollector(new LeafBucketCollectorBase(sub, globalOrds) {
127+
@Override
128+
public void collect(int doc, long owningBucketOrd) throws IOException {
129+
assert owningBucketOrd == 0;
130+
if (false == singleValues.advanceExact(doc)) {
131+
return;
132+
}
133+
int globalOrd = singleValues.ordValue();
134+
collectionStrategy.collectGlobalOrd(doc, globalOrd, sub);
135+
}
136+
});
137+
}
124138
return resultStrategy.wrapCollector(new LeafBucketCollectorBase(sub, globalOrds) {
125139
@Override
126140
public void collect(int doc, long owningBucketOrd) throws IOException {
127141
assert owningBucketOrd == 0;
128-
if (singleValues.advanceExact(doc)) {
129-
int ord = singleValues.ordValue();
130-
collectionStrategy.collectGlobalOrd(doc, ord, sub);
142+
if (false == singleValues.advanceExact(doc)) {
143+
return;
144+
}
145+
int globalOrd = singleValues.ordValue();
146+
if (false == acceptedGlobalOrdinals.test(globalOrd)) {
147+
return;
148+
}
149+
collectionStrategy.collectGlobalOrd(doc, globalOrd, sub);
150+
}
151+
});
152+
}
153+
segmentsWithMultiValuedOrds++;
154+
if (acceptedGlobalOrdinals == ALWAYS_TRUE) {
155+
/*
156+
* Optimize when there isn't a filter because that is very
157+
* common and marginally faster.
158+
*/
159+
return resultStrategy.wrapCollector(new LeafBucketCollectorBase(sub, globalOrds) {
160+
@Override
161+
public void collect(int doc, long owningBucketOrd) throws IOException {
162+
assert owningBucketOrd == 0;
163+
if (false == globalOrds.advanceExact(doc)) {
164+
return;
165+
}
166+
for (long globalOrd = globalOrds.nextOrd(); globalOrd != NO_MORE_ORDS; globalOrd = globalOrds.nextOrd()) {
167+
collectionStrategy.collectGlobalOrd(doc, globalOrd, sub);
131168
}
132169
}
133170
});
@@ -136,10 +173,14 @@ public void collect(int doc, long owningBucketOrd) throws IOException {
136173
@Override
137174
public void collect(int doc, long owningBucketOrd) throws IOException {
138175
assert owningBucketOrd == 0;
139-
if (globalOrds.advanceExact(doc)) {
140-
for (long globalOrd = globalOrds.nextOrd(); globalOrd != NO_MORE_ORDS; globalOrd = globalOrds.nextOrd()) {
141-
collectionStrategy.collectGlobalOrd(doc, globalOrd, sub);
176+
if (false == globalOrds.advanceExact(doc)) {
177+
return;
178+
}
179+
for (long globalOrd = globalOrds.nextOrd(); globalOrd != NO_MORE_ORDS; globalOrd = globalOrds.nextOrd()) {
180+
if (false == acceptedGlobalOrdinals.test(globalOrd)) {
181+
continue;
142182
}
183+
collectionStrategy.collectGlobalOrd(doc, globalOrd, sub);
143184
}
144185
}
145186
});
@@ -160,6 +201,9 @@ public void collectDebugInfo(BiConsumer<String, Object> add) {
160201
super.collectDebugInfo(add);
161202
add.accept("collection_strategy", collectionStrategy.describe());
162203
add.accept("result_strategy", resultStrategy.describe());
204+
add.accept("segments_with_single_valued_ords", segmentsWithSingleValuedOrds);
205+
add.accept("segments_with_multi_valued_ords", segmentsWithMultiValuedOrds);
206+
add.accept("has_filter", acceptedGlobalOrdinals != ALWAYS_TRUE);
163207
}
164208

165209
/**
@@ -253,26 +297,31 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, LeafBucketCol
253297
assert sub == LeafBucketCollector.NO_OP_COLLECTOR;
254298
final SortedDocValues singleValues = DocValues.unwrapSingleton(segmentOrds);
255299
mapping = valuesSource.globalOrdinalsMapping(ctx);
300+
// Dense mode doesn't support include/exclude so we don't have to check it here.
256301
if (singleValues != null) {
302+
segmentsWithSingleValuedOrds++;
257303
return resultStrategy.wrapCollector(new LeafBucketCollectorBase(sub, segmentOrds) {
258304
@Override
259305
public void collect(int doc, long owningBucketOrd) throws IOException {
260306
assert owningBucketOrd == 0;
261-
if (singleValues.advanceExact(doc)) {
262-
final int ord = singleValues.ordValue();
263-
segmentDocCounts.increment(ord + 1, 1);
307+
if (false == singleValues.advanceExact(doc)) {
308+
return;
264309
}
310+
int ord = singleValues.ordValue();
311+
segmentDocCounts.increment(ord + 1, 1);
265312
}
266313
});
267314
}
315+
segmentsWithMultiValuedOrds++;
268316
return resultStrategy.wrapCollector(new LeafBucketCollectorBase(sub, segmentOrds) {
269317
@Override
270318
public void collect(int doc, long owningBucketOrd) throws IOException {
271319
assert owningBucketOrd == 0;
272-
if (segmentOrds.advanceExact(doc)) {
273-
for (long segmentOrd = segmentOrds.nextOrd(); segmentOrd != NO_MORE_ORDS; segmentOrd = segmentOrds.nextOrd()) {
274-
segmentDocCounts.increment(segmentOrd + 1, 1);
275-
}
320+
if (false == segmentOrds.advanceExact(doc)) {
321+
return;
322+
}
323+
for (long segmentOrd = segmentOrds.nextOrd(); segmentOrd != NO_MORE_ORDS; segmentOrd = segmentOrds.nextOrd()) {
324+
segmentDocCounts.increment(segmentOrd + 1, 1);
276325
}
277326
}
278327
});
@@ -306,52 +355,6 @@ private void mapSegmentCountsToGlobalCounts(LongUnaryOperator mapping) throws IO
306355
}
307356
}
308357

309-
private static final class FilteredOrdinals extends AbstractSortedSetDocValues {
310-
311-
private final SortedSetDocValues inner;
312-
private final LongPredicate accepted;
313-
314-
private FilteredOrdinals(SortedSetDocValues inner, LongPredicate accepted) {
315-
this.inner = inner;
316-
this.accepted = accepted;
317-
}
318-
319-
@Override
320-
public long getValueCount() {
321-
return inner.getValueCount();
322-
}
323-
324-
@Override
325-
public BytesRef lookupOrd(long ord) throws IOException {
326-
return inner.lookupOrd(ord);
327-
}
328-
329-
@Override
330-
public long nextOrd() throws IOException {
331-
for (long ord = inner.nextOrd(); ord != NO_MORE_ORDS; ord = inner.nextOrd()) {
332-
if (accepted.test(ord)) {
333-
return ord;
334-
}
335-
}
336-
return NO_MORE_ORDS;
337-
}
338-
339-
@Override
340-
public boolean advanceExact(int target) throws IOException {
341-
if (inner.advanceExact(target)) {
342-
for (long ord = inner.nextOrd(); ord != NO_MORE_ORDS; ord = inner.nextOrd()) {
343-
if (accepted.test(ord)) {
344-
// reset the iterator
345-
boolean advanced = inner.advanceExact(target);
346-
assert advanced;
347-
return true;
348-
}
349-
}
350-
}
351-
return false;
352-
}
353-
}
354-
355358
/**
356359
* Strategy for collecting global ordinals.
357360
* <p>
@@ -800,4 +803,9 @@ private void oversizedCopy(BytesRef from, BytesRef to) {
800803
System.arraycopy(from.bytes, from.offset, to.bytes, 0, from.length);
801804
}
802805
}
806+
807+
/**
808+
* Predicate used for {@link #acceptedGlobalOrdinals} if there is no filter.
809+
*/
810+
private static final LongPredicate ALWAYS_TRUE = l -> true;
803811
}

0 commit comments

Comments
 (0)