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);