|
|
@@ -16,10 +16,14 @@ import org.apache.lucene.search.ScoreDoc;
|
|
|
import org.apache.lucene.search.ScoreMode;
|
|
|
import org.apache.lucene.search.Sort;
|
|
|
import org.apache.lucene.search.SortField;
|
|
|
+import org.apache.lucene.search.SortedNumericSortField;
|
|
|
+import org.apache.lucene.search.SortedSetSortField;
|
|
|
import org.apache.lucene.search.TopDocsCollector;
|
|
|
import org.apache.lucene.search.TopFieldCollectorManager;
|
|
|
import org.apache.lucene.search.TopScoreDocCollectorManager;
|
|
|
+import org.apache.lucene.util.RamUsageEstimator;
|
|
|
import org.elasticsearch.common.Strings;
|
|
|
+import org.elasticsearch.common.breaker.CircuitBreaker;
|
|
|
import org.elasticsearch.compute.data.BlockFactory;
|
|
|
import org.elasticsearch.compute.data.DocBlock;
|
|
|
import org.elasticsearch.compute.data.DocVector;
|
|
|
@@ -44,7 +48,11 @@ import java.util.function.Function;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
- * Source operator that builds Pages out of the output of a TopFieldCollector (aka TopN)
|
|
|
+ * Source operator that builds Pages out of the output of a TopFieldCollector (aka TopN).
|
|
|
+ * <p>
|
|
|
+ * Makes {@link Page}s of the shape {@code (docBlock)} or {@code (docBlock, score)}.
|
|
|
+ * Lucene loads the sort keys, but we don't read them from lucene. Yet. We should.
|
|
|
+ * </p>
|
|
|
*/
|
|
|
public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
|
|
|
@@ -52,6 +60,7 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
private final List<? extends ShardContext> contexts;
|
|
|
private final int maxPageSize;
|
|
|
private final List<SortBuilder<?>> sorts;
|
|
|
+ private final long estimatedPerRowSortSize;
|
|
|
|
|
|
public Factory(
|
|
|
List<? extends ShardContext> contexts,
|
|
|
@@ -61,6 +70,7 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
int maxPageSize,
|
|
|
int limit,
|
|
|
List<SortBuilder<?>> sorts,
|
|
|
+ long estimatedPerRowSortSize,
|
|
|
boolean needsScore
|
|
|
) {
|
|
|
super(
|
|
|
@@ -76,11 +86,22 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
this.contexts = contexts;
|
|
|
this.maxPageSize = maxPageSize;
|
|
|
this.sorts = sorts;
|
|
|
+ this.estimatedPerRowSortSize = estimatedPerRowSortSize;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public SourceOperator get(DriverContext driverContext) {
|
|
|
- return new LuceneTopNSourceOperator(contexts, driverContext.blockFactory(), maxPageSize, sorts, limit, sliceQueue, needsScore);
|
|
|
+ return new LuceneTopNSourceOperator(
|
|
|
+ contexts,
|
|
|
+ driverContext.breaker(),
|
|
|
+ driverContext.blockFactory(),
|
|
|
+ maxPageSize,
|
|
|
+ sorts,
|
|
|
+ estimatedPerRowSortSize,
|
|
|
+ limit,
|
|
|
+ sliceQueue,
|
|
|
+ needsScore
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
public int maxPageSize() {
|
|
|
@@ -104,10 +125,16 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private final CircuitBreaker breaker;
|
|
|
+ private final List<SortBuilder<?>> sorts;
|
|
|
+ private final long estimatedPerRowSortSize;
|
|
|
+ private final int limit;
|
|
|
+ private final boolean needsScore;
|
|
|
+
|
|
|
/**
|
|
|
- * Collected docs. {@code null} until we're {@link #emit(boolean)}.
|
|
|
+ * Collected docs. {@code null} until we're ready to {@link #emit()}.
|
|
|
*/
|
|
|
- private ScoreDoc[] scoreDocs;
|
|
|
+ private ScoreDoc[] topDocs;
|
|
|
|
|
|
/**
|
|
|
* {@link ShardRefCounted} for collected docs.
|
|
|
@@ -115,28 +142,30 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
private ShardRefCounted shardRefCounted;
|
|
|
|
|
|
/**
|
|
|
- * The offset in {@link #scoreDocs} of the next page.
|
|
|
+ * The offset in {@link #topDocs} of the next page.
|
|
|
*/
|
|
|
private int offset = 0;
|
|
|
|
|
|
private PerShardCollector perShardCollector;
|
|
|
- private final List<SortBuilder<?>> sorts;
|
|
|
- private final int limit;
|
|
|
- private final boolean needsScore;
|
|
|
|
|
|
public LuceneTopNSourceOperator(
|
|
|
List<? extends ShardContext> contexts,
|
|
|
+ CircuitBreaker breaker,
|
|
|
BlockFactory blockFactory,
|
|
|
int maxPageSize,
|
|
|
List<SortBuilder<?>> sorts,
|
|
|
+ long estimatedPerRowSortSize,
|
|
|
int limit,
|
|
|
LuceneSliceQueue sliceQueue,
|
|
|
boolean needsScore
|
|
|
) {
|
|
|
super(contexts, blockFactory, maxPageSize, sliceQueue);
|
|
|
+ this.breaker = breaker;
|
|
|
this.sorts = sorts;
|
|
|
+ this.estimatedPerRowSortSize = estimatedPerRowSortSize;
|
|
|
this.limit = limit;
|
|
|
this.needsScore = needsScore;
|
|
|
+ breaker.addEstimateBytesAndMaybeBreak(reserveSize(), "esql lucene topn");
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -147,7 +176,7 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
@Override
|
|
|
public void finish() {
|
|
|
doneCollecting = true;
|
|
|
- scoreDocs = null;
|
|
|
+ topDocs = null;
|
|
|
shardRefCounted = null;
|
|
|
assert isFinished();
|
|
|
}
|
|
|
@@ -160,7 +189,7 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
long start = System.nanoTime();
|
|
|
try {
|
|
|
if (isEmitting()) {
|
|
|
- return emit(false);
|
|
|
+ return emit();
|
|
|
} else {
|
|
|
return collect();
|
|
|
}
|
|
|
@@ -174,7 +203,8 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
var scorer = getCurrentOrLoadNextScorer();
|
|
|
if (scorer == null) {
|
|
|
doneCollecting = true;
|
|
|
- return emit(true);
|
|
|
+ startEmitting();
|
|
|
+ return emit();
|
|
|
}
|
|
|
try {
|
|
|
if (scorer.tags().isEmpty() == false) {
|
|
|
@@ -193,32 +223,47 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
if (scorer.isDone()) {
|
|
|
var nextScorer = getCurrentOrLoadNextScorer();
|
|
|
if (nextScorer == null || nextScorer.shardContext().index() != scorer.shardContext().index()) {
|
|
|
- return emit(true);
|
|
|
+ startEmitting();
|
|
|
+ return emit();
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
private boolean isEmitting() {
|
|
|
- return scoreDocs != null && offset < scoreDocs.length;
|
|
|
+ return topDocs != null;
|
|
|
}
|
|
|
|
|
|
- private Page emit(boolean startEmitting) {
|
|
|
- if (startEmitting) {
|
|
|
- assert isEmitting() == false : "offset=" + offset + " score_docs=" + Arrays.toString(scoreDocs);
|
|
|
- offset = 0;
|
|
|
- if (perShardCollector != null) {
|
|
|
- scoreDocs = perShardCollector.collector.topDocs().scoreDocs;
|
|
|
- int shardId = perShardCollector.shardContext.index();
|
|
|
- shardRefCounted = new ShardRefCounted.Single(shardId, shardContextCounters.get(shardId));
|
|
|
- } else {
|
|
|
- scoreDocs = new ScoreDoc[0];
|
|
|
- }
|
|
|
+ private void startEmitting() {
|
|
|
+ assert isEmitting() == false : "offset=" + offset + " score_docs=" + Arrays.toString(topDocs);
|
|
|
+ offset = 0;
|
|
|
+ if (perShardCollector != null) {
|
|
|
+ /*
|
|
|
+ * Important note for anyone who looks at this and has bright ideas:
|
|
|
+ * There *is* a method in lucene to return topDocs with an offset
|
|
|
+ * and a limit. So you'd *think* you can scroll the top docs there.
|
|
|
+ * But you can't. It's expressly forbidden to call any of the `topDocs`
|
|
|
+ * methods more than once. You *must* call `topDocs` once and use the
|
|
|
+ * array.
|
|
|
+ */
|
|
|
+ topDocs = perShardCollector.collector.topDocs().scoreDocs;
|
|
|
+ int shardId = perShardCollector.shardContext.index();
|
|
|
+ shardRefCounted = new ShardRefCounted.Single(shardId, shardContextCounters.get(shardId));
|
|
|
+ } else {
|
|
|
+ topDocs = new ScoreDoc[0];
|
|
|
}
|
|
|
- if (offset >= scoreDocs.length) {
|
|
|
+ }
|
|
|
+
|
|
|
+ private void stopEmitting() {
|
|
|
+ topDocs = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Page emit() {
|
|
|
+ if (offset >= topDocs.length) {
|
|
|
+ stopEmitting();
|
|
|
return null;
|
|
|
}
|
|
|
- int size = Math.min(maxPageSize, scoreDocs.length - offset);
|
|
|
+ int size = Math.min(maxPageSize, topDocs.length - offset);
|
|
|
IntBlock shard = null;
|
|
|
IntVector segments = null;
|
|
|
IntVector docs = null;
|
|
|
@@ -234,14 +279,16 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
offset += size;
|
|
|
List<LeafReaderContext> leafContexts = perShardCollector.shardContext.searcher().getLeafContexts();
|
|
|
for (int i = start; i < offset; i++) {
|
|
|
- int doc = scoreDocs[i].doc;
|
|
|
+ int doc = topDocs[i].doc;
|
|
|
int segment = ReaderUtil.subIndex(doc, leafContexts);
|
|
|
currentSegmentBuilder.appendInt(segment);
|
|
|
currentDocsBuilder.appendInt(doc - leafContexts.get(segment).docBase); // the offset inside the segment
|
|
|
if (currentScoresBuilder != null) {
|
|
|
- float score = getScore(scoreDocs[i]);
|
|
|
+ float score = getScore(topDocs[i]);
|
|
|
currentScoresBuilder.appendDouble(score);
|
|
|
}
|
|
|
+ // Null the top doc so it can be GCed early, just in case.
|
|
|
+ topDocs[i] = null;
|
|
|
}
|
|
|
|
|
|
int shardId = perShardCollector.shardContext.index();
|
|
|
@@ -298,9 +345,20 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
sb.append(", sorts = [").append(notPrettySorts).append("]");
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ protected void additionalClose() {
|
|
|
+ Releasables.close(() -> breaker.addWithoutBreaking(-reserveSize()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private long reserveSize() {
|
|
|
+ long perRowSize = FIELD_DOC_SIZE + estimatedPerRowSortSize;
|
|
|
+ return limit * perRowSize;
|
|
|
+ }
|
|
|
+
|
|
|
abstract static class PerShardCollector {
|
|
|
private final ShardContext shardContext;
|
|
|
private final TopDocsCollector<?> collector;
|
|
|
+
|
|
|
private int leafIndex;
|
|
|
private LeafCollector leafCollector;
|
|
|
private Thread currentThread;
|
|
|
@@ -366,4 +424,68 @@ public final class LuceneTopNSourceOperator extends LuceneOperator {
|
|
|
sort = new Sort(l.toArray(SortField[]::new));
|
|
|
return new ScoringPerShardCollector(context, new TopFieldCollectorManager(sort, limit, null, 0).newCollector());
|
|
|
}
|
|
|
+
|
|
|
+ private static int perDocMemoryUsage(SortField[] sorts) {
|
|
|
+ int usage = FIELD_DOC_SIZE;
|
|
|
+ for (SortField sort : sorts) {
|
|
|
+ usage += perDocMemoryUsage(sort);
|
|
|
+ }
|
|
|
+ return usage;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int perDocMemoryUsage(SortField sort) {
|
|
|
+ if (sort.getType() == SortField.Type.CUSTOM) {
|
|
|
+ return perDocMemoryUsageForCustom(sort);
|
|
|
+ }
|
|
|
+ return perDocMemoryUsageByType(sort, sort.getType());
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int perDocMemoryUsageByType(SortField sort, SortField.Type type) {
|
|
|
+ return switch (type) {
|
|
|
+ case SCORE, DOC ->
|
|
|
+ /* SCORE and DOC are always part of ScoreDoc/FieldDoc
|
|
|
+ * So they are in FIELD_DOC_SIZE already.
|
|
|
+ * And they can't be removed. */
|
|
|
+ 0;
|
|
|
+ case DOUBLE, LONG ->
|
|
|
+ // 8 for the long, 8 for the long copied to the topDoc.
|
|
|
+ 16;
|
|
|
+ case INT, FLOAT ->
|
|
|
+ // 4 for the int, 8 boxed object copied to topDoc.
|
|
|
+ 12;
|
|
|
+ case STRING ->
|
|
|
+ /* `keyword`-like fields. Compares ordinals when possible, otherwise
|
|
|
+ * the strings. Does a bunch of deduplication, but in the worst
|
|
|
+ * case we end up with the string itself, plus two BytesRefs. Let's
|
|
|
+ * presume short-ish strings. */
|
|
|
+ 1024;
|
|
|
+ case STRING_VAL ->
|
|
|
+ /* Other string fields. Compares the string itself. Let's assume two
|
|
|
+ * 2kb per string because they tend to be bigger than the keyword
|
|
|
+ * versions. */
|
|
|
+ 2048;
|
|
|
+ case CUSTOM -> throw new IllegalArgumentException("unsupported type " + sort.getClass() + ": " + sort);
|
|
|
+ case REWRITEABLE -> {
|
|
|
+ assert false : "rewriteable " + sort.getClass() + ": " + sort;
|
|
|
+ yield 2048;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int perDocMemoryUsageForCustom(SortField sort) {
|
|
|
+ return switch (sort) {
|
|
|
+ case SortedNumericSortField f -> perDocMemoryUsageByType(f, f.getNumericType());
|
|
|
+ case SortedSetSortField f -> perDocMemoryUsageByType(f, SortField.Type.STRING);
|
|
|
+ default -> {
|
|
|
+ if (sort.getClass().getName().equals("org.apache.lucene.document.LatLonPointSortField")) {
|
|
|
+ yield perDocMemoryUsageByType(sort, SortField.Type.DOUBLE);
|
|
|
+ }
|
|
|
+ assert false : "unknown type " + sort.getClass() + ": " + sort;
|
|
|
+ yield 2048;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final int FIELD_DOC_SIZE = Math.toIntExact(RamUsageEstimator.shallowSizeOf(FieldDoc.class));
|
|
|
}
|