You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by mk...@apache.org on 2026/09/26 12:43:56 UTC
(solr) branch main updated: SOLR-18484: {!auxIndexJoin} eager drain when there's no a sibling filter (#4949)
This is an automated email from the ASF dual-hosted git repository.
mkhludnev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 6c245f0bcd7 SOLR-18484: {!auxIndexJoin} eager drain when there's no a sibling filter (#4949)
6c245f0bcd7 is described below
commit 6c245f0bcd75ccd25a681e6278f4c08bcb25f83f
Author: Mikhail Khludnev <mk...@apache.org>
AuthorDate: Sat Sep 26 15:43:51 2026 +0300
SOLR-18484: {!auxIndexJoin} eager drain when there's no a sibling filter (#4949)
---
.../SOLR-18484-auxIndexJoin-faster-no-filter.yml | 9 ++
.../join/auxindexjoin/JoinIndexScorerSupplier.java | 134 +++++++++++++++------
2 files changed, 103 insertions(+), 40 deletions(-)
diff --git a/changelog/unreleased/SOLR-18484-auxIndexJoin-faster-no-filter.yml b/changelog/unreleased/SOLR-18484-auxIndexJoin-faster-no-filter.yml
new file mode 100644
index 00000000000..3fd239ac2bd
--- /dev/null
+++ b/changelog/unreleased/SOLR-18484-auxIndexJoin-faster-no-filter.yml
@@ -0,0 +1,9 @@
+title: >
+ Faster {!auxIndexJoin} when there's no filter.
+type: changed
+authors:
+ - name: Mikhail Khludnev
+ nick: mkhl
+links:
+ - name: SOLR-18484
+ url: https://issues.apache.org/jira/browse/SOLR-18484
diff --git a/solr/core/src/java/org/apache/solr/search/join/auxindexjoin/JoinIndexScorerSupplier.java b/solr/core/src/java/org/apache/solr/search/join/auxindexjoin/JoinIndexScorerSupplier.java
index 3f725cb09c4..9fa09423676 100644
--- a/solr/core/src/java/org/apache/solr/search/join/auxindexjoin/JoinIndexScorerSupplier.java
+++ b/solr/core/src/java/org/apache/solr/search/join/auxindexjoin/JoinIndexScorerSupplier.java
@@ -76,7 +76,8 @@ import org.slf4j.LoggerFactory;
* shape.
* <li>{@code evt=ctx} -- this context finished setting up: how many (from, to) pairs contributed,
* how many the a-priori from-edge check dropped before any column was opened, and how loose
- * the resulting approximation is.
+ * the resulting approximation is. Emitted from {@link #get}, with {@code mode=eager} when
+ * every column is drained up front instead of confirmed lazily.
* <li>{@code evt=drain} -- one join column was read through during confirmation, and whether that
* read confirmed the doc under test (an early exit) or not.
* <li>{@code evt=done} -- confirmation reached a terminal state for this context: every column
@@ -216,12 +217,12 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
joinTask.dumpMatchesInto(falseNegToDocsBits, shift, approximation.docID());
fromDocsWalked += joinTask.lastDrainWalked();
if (outcome == DrainOutcome.CONFIRMED) {
- logDrain(joinTask, true);
+ logDrain(joinTask, true, falseNegToDocsBits);
return true;
}
JoinIndexScorerSupplier.this.dropJoinLeaf(joinTask);
leafsDrained++;
- logDrain(joinTask, false);
+ logDrain(joinTask, false, falseNegToDocsBits);
}
} catch (ExecutionException e) {
throw new RuntimeException(e);
@@ -243,36 +244,35 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
return matched;
}
- /**
- * Reports one column read. {@code confirmed=true} is the early exit the lazy variant exists
- * for: the doc under test was found before the remaining {@code cellsLeft} columns were
- * touched.
- */
- private void logDrain(LeafJoin joinTask, boolean confirmed) {
- if (!JoinIndexUtils.diagnosticsEnabled(log)) {
- return;
- }
- JoinIndexUtils.logDiagnostic(
- log,
- "AUXIJOIN evt=drain ctx={} toSeg={} pair={} confirmed={} walked={} colToCount={}"
- + " cellsLeft={} hCard={} confirmCalls={}",
- ctxId,
- JoinIndexUtils.segmentName(toContext),
- joinTask.pairFieldName,
- confirmed,
- joinTask.lastDrainWalked(),
- joinTask.toCount(),
- leafJoins.size(),
- falseNegToDocsBits == null ? 0 : falseNegToDocsBits.cardinality(),
- confirmCalls);
- }
-
@Override
public float matchCost() {
return matchedToDocsCount;
}
}
+ /**
+ * Reports one column read. {@code confirmed=true} is the early exit the lazy variant exists for:
+ * the doc under test was found before the remaining {@code cellsLeft} columns were touched.
+ */
+ private void logDrain(LeafJoin joinTask, boolean confirmed, FixedBitSet union) {
+ if (!JoinIndexUtils.diagnosticsEnabled(log)) {
+ return;
+ }
+ JoinIndexUtils.logDiagnostic(
+ log,
+ "AUXIJOIN evt=drain ctx={} toSeg={} pair={} confirmed={} walked={} colToCount={}"
+ + " cellsLeft={} hCard={} confirmCalls={}",
+ ctxId,
+ JoinIndexUtils.segmentName(toContext),
+ joinTask.pairFieldName,
+ confirmed,
+ joinTask.lastDrainWalked(),
+ joinTask.toCount(),
+ leafJoins.size(),
+ union == null ? 0 : union.cardinality(),
+ confirmCalls);
+ }
+
/**
* Represents a cell in the join matrix bounded to from and to segments. Resolved exactly once,
* either from the join index ({@link #resolveFromReadingJoinIndex}: just the pair's {@link
@@ -636,18 +636,24 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
// array-backed,
advanceAtMinFromEdge(task);
}
- // now let's read each cell's edges, then build "to" side bitset of approximation
- // first pass: union the contributing pairs' to-doc ranges; every possible match in this
- // to segment falls into [minToDoc, maxToDoc]
+ // bounds of the contributing pairs' to-doc ranges, which cost() needs before get(); every
+ // possible match in this to segment falls into [firstToDoc, lastToDoc]
for (LeafJoin task : leafJoins) {
DocEdges docEdges = task;
firstToDoc = Math.min(firstToDoc, docEdges.toDocEdges()[0]);
lastToDoc = Math.max(lastToDoc, docEdges.toDocEdges()[1]);
matchedToDocsCount += docEdges.toDocEdges()[1] - docEdges.toDocEdges()[0] + 1;
- if (falsePositiveToDocsBits == null) {
- falsePositiveToDocsBits = new FixedBitSet(toContext.reader().maxDoc());
- }
- falsePositiveToDocsBits.set(docEdges.toDocEdges()[0], docEdges.toDocEdges()[1] + 1);
+ }
+ }
+
+ /**
+ * Builds the approximation -- the union of the surviving pairs' to-ranges -- and orders the cells
+ * for {@link LazyConfirmationIterator}.
+ */
+ private void setUpLazyConfirmation() {
+ falsePositiveToDocsBits = new FixedBitSet(toContext.reader().maxDoc());
+ for (LeafJoin task : leafJoins) {
+ falsePositiveToDocsBits.set(task.toDocEdges()[0], task.toDocEdges()[1] + 1);
}
// when confirming docs, we want to start from heaviest leafs first
if (leafJoins.size() > 1) {
@@ -662,7 +668,36 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
leafJoin.edges.toCount())))
.reversed());
}
- logContextSetUp();
+ }
+
+ /**
+ * Drains every cell through into an exact bitset of this segment's matches, one column after
+ * another. Used when nothing will thin the approximation, so {@link LazyConfirmationIterator}
+ * would end up confirming every doc of it anyway -- at a searcher acquire and a refresh per miss.
+ */
+ private FixedBitSet drainAll() throws IOException {
+ FixedBitSet matched = new FixedBitSet(toContext.reader().maxDoc());
+ IndexSearcher freshSearcher = joinIndex.acquire();
+ try {
+ TaskRefreshResult refreshed = refreshJoinTasksReferences(freshSearcher);
+ for (Entry<LeafJoin, JoinColumnModel> entry : refreshed.justWritten) {
+ entry.getKey().bind(entry.getValue());
+ }
+ for (LeafJoin cell : new ArrayList<>(leafJoins)) {
+ // no doc is under confirmation: -1 keeps every to-doc and never exits early
+ DrainOutcome outcome = cell.dumpMatchesInto(matched, 0, -1);
+ assert outcome == DrainOutcome.EXHAUSTED;
+ fromDocsWalked += cell.lastDrainWalked();
+ dropJoinLeaf(cell);
+ leafsDrained++;
+ logDrain(cell, false, matched);
+ }
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ joinIndex.release(freshSearcher);
+ }
+ return matched;
}
/**
@@ -670,9 +705,10 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
* approximation (the union of the surviving pairs' to-ranges), while {@code approxSpanSum} adds
* those ranges up with their overlaps counted twice; the two together say how much the
* single-range-per-column approximation actually narrows the segment, which is what bounds every
- * saving the confirmation phase can make.
+ * saving the confirmation phase can make. {@code mode} is {@code lazy} or {@code eager}, see
+ * {@link #get}; an eager context builds no approximation, so its {@code approxCard} is 0.
*/
- private void logContextSetUp() {
+ private void logContextSetUp(String mode) {
if (!JoinIndexUtils.diagnosticsEnabled(log)) {
return;
}
@@ -682,11 +718,12 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
}
JoinIndexUtils.logDiagnostic(
log,
- "AUXIJOIN evt=ctx ctx={} toSeg={} toMaxDoc={} cellsCreated={} cellsDroppedApriori={}"
- + " cellsLive={} buildMs={} approxCard={} approxSpanSum={} approxFrom={} approxTo={}"
+ "AUXIJOIN evt=ctx ctx={} toSeg={} mode={} toMaxDoc={} cellsCreated={}"
+ + " cellsDroppedApriori={} cellsLive={} buildMs={} approxCard={} approxSpanSum={} approxFrom={} approxTo={}"
+ " colToCountSum={}",
ctxId,
JoinIndexUtils.segmentName(toContext),
+ mode,
toContext.reader().maxDoc(),
joinLeafsCreated,
leafsDroppedApriori,
@@ -1015,12 +1052,29 @@ class JoinIndexScorerSupplier extends ScorerSupplier {
/** True if this context has any candidate docs at all; used by {@code JoinIndexWeight}. */
boolean isEmpty() {
- return falsePositiveToDocsBits == null || matchedToDocsCount == 0;
+ return leafJoins.isEmpty();
}
+ /**
+ * With a {@code leadCost} of {@link Long#MAX_VALUE} nothing leads this clause, so every doc of
+ * the approximation would be confirmed anyway: the columns are drained eagerly into an exact,
+ * single-phase iterator. Otherwise the approximation is handed out and each doc a leading clause
+ * lands on is confirmed lazily.
+ */
@Override
public Scorer get(long leadCost) throws IOException {
assert !isEmpty();
+ if (leadCost == Long.MAX_VALUE) {
+ logContextSetUp("eager");
+ FixedBitSet matched = drainAll();
+ logConfirmationDone("eager");
+ int card = matched.cardinality();
+ DocIdSetIterator exact =
+ card == 0 ? DocIdSetIterator.empty() : new BitSetIterator(matched, card);
+ return new ConstantScoreScorer(boost, scoreMode, exact);
+ }
+ setUpLazyConfirmation();
+ logContextSetUp("lazy");
DocIdSetIterator approximation =
new BitSetIterator(falsePositiveToDocsBits, matchedToDocsCount);