Skip to content

Commit b072f5f

Browse files
authored
Fix an optimization in terms agg (#57438)
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 3bb11cf commit b072f5f

File tree

2 files changed

+153
-70
lines changed

2 files changed

+153
-70
lines changed

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

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -724,10 +724,10 @@ setup:
724724
body: { "size" : 0, "aggs" : { "no_field_terms" : { "terms" : { "size": 1 } } } }
725725

726726
---
727-
"profiler":
727+
"global ords profiler":
728728
- skip:
729-
version: " - 7.8.99"
730-
reason: debug information added in 7.9.0
729+
version: " - 7.9.99"
730+
reason: debug information added in 8.0.0 (backport to 7.9.0 pending)
731731
- do:
732732
bulk:
733733
index: test_1
@@ -753,6 +753,7 @@ setup:
753753
terms:
754754
field: str
755755
collect_mode: breadth_first
756+
execution_hint: global_ordinals
756757
aggs:
757758
max_number:
758759
max:
@@ -768,9 +769,83 @@ setup:
768769
- match: { profile.shards.0.aggregations.0.breakdown.collect_count: 4 }
769770
- match: { profile.shards.0.aggregations.0.debug.deferred_aggregators: [ max_number ] }
770771
- match: { profile.shards.0.aggregations.0.debug.collection_strategy: dense }
772+
- match: { profile.shards.0.aggregations.0.debug.result_strategy: terms }
773+
- gt: { profile.shards.0.aggregations.0.debug.segments_with_single_valued_ords: 0 }
774+
- match: { profile.shards.0.aggregations.0.debug.segments_with_multi_valued_ords: 0 }
775+
- match: { profile.shards.0.aggregations.0.debug.has_filter: false }
771776
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
772777
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
773778

779+
- do:
780+
indices.create:
781+
index: test_3
782+
body:
783+
settings:
784+
number_of_shards: 1
785+
number_of_replicas: 0
786+
mappings:
787+
properties:
788+
str:
789+
type: keyword
790+
- do:
791+
bulk:
792+
index: test_3
793+
refresh: true
794+
body: |
795+
{ "index": {} }
796+
{ "str": ["pig", "sheep"], "number": 100 }
797+
798+
- do:
799+
search:
800+
index: test_3
801+
body:
802+
profile: true
803+
size: 0
804+
aggs:
805+
str_terms:
806+
terms:
807+
field: str
808+
collect_mode: breadth_first
809+
execution_hint: global_ordinals
810+
aggs:
811+
max_number:
812+
max:
813+
field: number
814+
- match: { aggregations.str_terms.buckets.0.key: pig }
815+
- match: { aggregations.str_terms.buckets.0.max_number.value: 100 }
816+
- match: { aggregations.str_terms.buckets.1.key: sheep }
817+
- match: { aggregations.str_terms.buckets.1.max_number.value: 100 }
818+
- match: { profile.shards.0.aggregations.0.type: GlobalOrdinalsStringTermsAggregator }
819+
- match: { profile.shards.0.aggregations.0.description: str_terms }
820+
- match: { profile.shards.0.aggregations.0.breakdown.collect_count: 1 }
821+
- match: { profile.shards.0.aggregations.0.debug.deferred_aggregators: [ max_number ] }
822+
- match: { profile.shards.0.aggregations.0.debug.collection_strategy: dense }
823+
- match: { profile.shards.0.aggregations.0.debug.result_strategy: terms }
824+
- match: { profile.shards.0.aggregations.0.debug.segments_with_single_valued_ords: 0 }
825+
- gt: { profile.shards.0.aggregations.0.debug.segments_with_multi_valued_ords: 0 }
826+
- match: { profile.shards.0.aggregations.0.debug.has_filter: false }
827+
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
828+
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
829+
830+
---
831+
"numeric profiler":
832+
- skip:
833+
version: " - 7.8.99"
834+
reason: debug information added in 7.9.0
835+
- do:
836+
bulk:
837+
index: test_1
838+
refresh: true
839+
body: |
840+
{ "index": {} }
841+
{ "number": 1 }
842+
{ "index": {} }
843+
{ "number": 3 }
844+
{ "index": {} }
845+
{ "number": 1 }
846+
{ "index": {} }
847+
{ "number": 1 }
848+
774849
- do:
775850
search:
776851
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)