This is an automated email from the ASF dual-hosted git repository.

dsmiley 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 b27b587  SOLR-14166 fq cache=false should use TwoPhaseIterator (#57)
b27b587 is described below

commit b27b58704c84b67375080a6e9f818fb7987c203b
Author: David Smiley <[email protected]>
AuthorDate: Sat May 1 18:48:52 2021 -0400

    SOLR-14166 fq cache=false should use TwoPhaseIterator (#57)
    
    instead of custom leapfrogging iterator thing, FilterImpl.
    
    * Simplify SolrIndexSearcher.getDocSet and makeBitDocSet
    * ExtendedQuery: remove getCacheSep & setCacheSep which never did anything
    * Javadocs: Clarify ExtendedQuery vs PostFilter
    * Add MatchCostQuery wrapper so that the cost local-param to "fq" is used 
for TPI.matchCost
---
 solr/CHANGES.txt                                   |   3 +
 .../solr/search/CollapsingQParserPlugin.java       |   8 -
 .../java/org/apache/solr/search/ExtendedQuery.java |  27 +-
 .../org/apache/solr/search/ExtendedQueryBase.java  |  14 -
 .../solr/search/GraphTermsQParserPlugin.java       |  17 +-
 .../src/java/org/apache/solr/search/Grouping.java  |  14 +-
 .../org/apache/solr/search/MatchCostQuery.java     | 158 ++++++++++
 .../src/java/org/apache/solr/search/QParser.java   |   2 -
 .../apache/solr/search/SolrConstantScoreQuery.java |   9 -
 .../org/apache/solr/search/SolrIndexSearcher.java  | 328 ++++-----------------
 .../src/test/org/apache/solr/core/SOLR749Test.java |  13 +-
 .../apache/solr/search/SolrIndexSearcherTest.java  |   9 -
 12 files changed, 271 insertions(+), 331 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d73b82c..1488563 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -283,6 +283,9 @@ Other Changes
   use.  The Jaeger plugin is now completely configurable via system properties 
or environment vars,
   not by values in solr.xml. (David Smiley)
 
+* SOLR-14166: Non-cached filter queries are now pushed down to Lucene, 
possibly benefiting from
+  TwoPhaseIterator, which can make a big difference for some queries.  (David 
Smiley)
+
 * SOLR-15369: PackageStoreAPI will only be loaded in SolrCloud mode (Mike Drob)
 
 Bug Fixes
diff --git 
a/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java 
b/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
index 9709f68..e1a64b0 100644
--- a/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
@@ -310,14 +310,6 @@ public class CollapsingQParserPlugin extends QParserPlugin 
{
 
     }
 
-    public void setCacheSep(boolean cacheSep) {
-
-    }
-
-    public boolean getCacheSep() {
-      return false;
-    }
-
     public boolean getCache() {
       return false;
     }
diff --git a/solr/core/src/java/org/apache/solr/search/ExtendedQuery.java 
b/solr/core/src/java/org/apache/solr/search/ExtendedQuery.java
index 5d1c069..e7346f8 100644
--- a/solr/core/src/java/org/apache/solr/search/ExtendedQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/ExtendedQuery.java
@@ -16,6 +16,9 @@
  */
 package org.apache.solr.search;
 
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TwoPhaseIterator;
+
 /** The ExtendedQuery interface provides extra metadata to a query.
  *  Implementations of ExtendedQuery must also extend Query.
  */
@@ -25,15 +28,27 @@ public interface ExtendedQuery {
 
   public void setCache(boolean cache);
 
-  /** Returns the cost of this query, used to order checking of filters that 
are not cached.
-   * If getCache()==false &amp;&amp; getCost()&gt;=100 &amp;&amp; this 
instanceof PostFilter, then
-   * the PostFilter interface will be used for filtering.
+  /**
+   * Returns the cost of this query, used to order checking of filters that 
are not cached. If
+   * getCache()==false &amp;&amp; getCost()&gt;=100 &amp;&amp; this instanceof 
PostFilter, then the
+   * PostFilter interface will be used for filtering.  Otherwise, for smaller 
costs, this cost will
+   * be used for {@link TwoPhaseIterator#matchCost()}.
    */
   public int getCost();
 
   public void setCost(int cost);
 
-  /** If true, the clauses of this boolean query should be cached separately. 
This is not yet implemented. */
-  public boolean getCacheSep();
-  public void setCacheSep(boolean cacheSep);
+  /**
+   * Returns this Query, applying {@link QueryUtils#makeQueryable(Query)} and 
maybe wrapping it to
+   * apply the {@link #getCost()} if it's above zero. Subclasses may customize 
this if the query
+   * internally applies the configurable cost on the underlying {@link
+   * TwoPhaseIterator#matchCost()}.
+   */
+  public default Query getCostAppliedQuery() {
+    Query me = QueryUtils.makeQueryable((Query) this);
+    if (getCost() <= 0) {
+      return me;
+    }
+    return new MatchCostQuery(me, getCost());
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/search/ExtendedQueryBase.java 
b/solr/core/src/java/org/apache/solr/search/ExtendedQueryBase.java
index 06d2401..2a4a386 100644
--- a/solr/core/src/java/org/apache/solr/search/ExtendedQueryBase.java
+++ b/solr/core/src/java/org/apache/solr/search/ExtendedQueryBase.java
@@ -21,7 +21,6 @@ import org.apache.lucene.search.Query;
 public abstract class ExtendedQueryBase extends Query implements ExtendedQuery 
{
   private int cost;
   private boolean cache = true;
-  private boolean cacheSep;
 
   @Override
   public void setCache(boolean cache) {
@@ -34,16 +33,6 @@ public abstract class ExtendedQueryBase extends Query 
implements ExtendedQuery {
   }
 
   @Override
-  public void setCacheSep(boolean cacheSep) {
-    this.cacheSep = cacheSep;
-  }
-
-  @Override
-  public boolean getCacheSep() {
-    return cacheSep;
-  }
-
-  @Override
   public void setCost(int cost) {
     this.cost = cost;
   }
@@ -67,9 +56,6 @@ public abstract class ExtendedQueryBase extends Query 
implements ExtendedQuery {
         sb.append(q.getCost());
       }
       sb.append("}");
-    } else if (q.getCacheSep()) {
-      sb.append("{!cache=sep");
-      sb.append("}");
     }
     return sb.toString();
   }
diff --git 
a/solr/core/src/java/org/apache/solr/search/GraphTermsQParserPlugin.java 
b/solr/core/src/java/org/apache/solr/search/GraphTermsQParserPlugin.java
index 2d3573d..151b660 100644
--- a/solr/core/src/java/org/apache/solr/search/GraphTermsQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/GraphTermsQParserPlugin.java
@@ -155,7 +155,9 @@ public class GraphTermsQParserPlugin extends QParserPlugin {
     };
   }
 
+  /** Similar to {@code TermsQuery} but adds a {@code maxDocFreq}. */
   private class GraphTermsQuery extends Query implements ExtendedQuery {
+    // Not a post filter. This will typically be used as the main query.
 
     private Term[] queryTerms;
     private String field;
@@ -181,20 +183,13 @@ public class GraphTermsQParserPlugin extends 
QParserPlugin {
       return false;
     }
 
-    public boolean getCacheSep() {
-      return false;
-    }
-
-    public void setCacheSep(boolean sep) {
-
-    }
-
     public void setCache(boolean cache) {
-
+      // TODO support user choice
     }
 
     public int getCost() {
-      return 1; // Not a post filter. The GraphTermsQuery will typically be 
used as the main query.
+      // 0 is the default and keeping it avoids a needless wrapper for 
TwoPhaseIterator matchCost.
+      return 0;
     }
 
     public void setCost(int cost) {
@@ -325,7 +320,7 @@ public class GraphTermsQParserPlugin extends QParserPlugin {
 
 
 
-// modified version of PointInSetQuery
+/** Modified version of {@code PointInSetQuery} to support {@code maxDocFreq}. 
*/
 abstract class PointSetQuery extends Query implements DocSetProducer, 
Accountable {
   protected static final long BASE_RAM_BYTES = 
RamUsageEstimator.shallowSizeOfInstance(PointSetQuery.class);
 
diff --git a/solr/core/src/java/org/apache/solr/search/Grouping.java 
b/solr/core/src/java/org/apache/solr/search/Grouping.java
index 343461d..8bbaf4d 100644
--- a/solr/core/src/java/org/apache/solr/search/Grouping.java
+++ b/solr/core/src/java/org/apache/solr/search/Grouping.java
@@ -102,8 +102,6 @@ public class Grouping {
   private boolean getGroupedDocSet;
   private boolean getDocList; // doclist needed for debugging or highlighting
   private Query query;
-  private DocSet filter;
-  private Filter luceneFilter;
   private NamedList<Object> grouped = new SimpleOrderedMap<>();
   private Set<Integer> idSet = new LinkedHashSet<>();  // used for tracking 
unique docs when we need a doclist
   private int maxMatches;  // max number of matches from any grouping command
@@ -304,7 +302,7 @@ public class Grouping {
     qr.setDocListAndSet(out);
 
     SolrIndexSearcher.ProcessedFilter pf = 
searcher.getProcessedFilter(cmd.getFilter(), cmd.getFilterList());
-    final Filter luceneFilter = pf.filter;
+    final Query filterQuery = pf.filter;
     maxDoc = searcher.maxDoc();
 
     needScores = (cmd.getFlags() & SolrIndexSearcher.GET_SCORES) != 0;
@@ -359,7 +357,7 @@ public class Grouping {
     }
 
     if (allCollectors != null) {
-      searchWithTimeLimiter(luceneFilter, allCollectors);
+      searchWithTimeLimiter(filterQuery, allCollectors);
 
       if(allCollectors instanceof DelegatingCollector) {
         ((DelegatingCollector) allCollectors).finish();
@@ -389,14 +387,14 @@ public class Grouping {
             signalCacheWarning = true;
             log.warn(String.format(Locale.ROOT, "The grouping cache is active, 
but not used because it exceeded the max cache limit of %d percent", 
maxDocsPercentageToCache));
             log.warn("Please increase cache size or disable group caching.");
-            searchWithTimeLimiter(luceneFilter, secondPhaseCollectors);
+            searchWithTimeLimiter(filterQuery, secondPhaseCollectors);
           }
         } else {
           if (pf.postFilter != null) {
             pf.postFilter.setLastDelegate(secondPhaseCollectors);
             secondPhaseCollectors = pf.postFilter;
           }
-          searchWithTimeLimiter(luceneFilter, secondPhaseCollectors);
+          searchWithTimeLimiter(filterQuery, secondPhaseCollectors);
         }
         if (secondPhaseCollectors instanceof DelegatingCollector) {
           ((DelegatingCollector) secondPhaseCollectors).finish();
@@ -425,7 +423,7 @@ public class Grouping {
    * Invokes search with the specified filter and collector.  
    * If a time limit has been specified, wrap the collector in a 
TimeLimitingCollector
    */
-  private void searchWithTimeLimiter(final Filter luceneFilter, Collector 
collector) throws IOException {
+  private void searchWithTimeLimiter(final Query filterQuery, Collector 
collector) throws IOException {
     if (cmd.getTimeAllowed() > 0) {
       if (timeLimitingCollector == null) {
         timeLimitingCollector = new TimeLimitingCollector(collector, 
TimeLimitingCollector.getGlobalCounter(), cmd.getTimeAllowed());
@@ -441,7 +439,7 @@ public class Grouping {
       collector = timeLimitingCollector;
     }
     try {
-      searcher.search(QueryUtils.combineQueryAndFilter(query, luceneFilter), 
collector);
+      searcher.search(QueryUtils.combineQueryAndFilter(query, filterQuery), 
collector);
     } catch (TimeLimitingCollector.TimeExceededException | 
ExitableDirectoryReader.ExitingReaderException x) {
       log.warn("Query: {}; ", query, x);
       qr.setPartialResults(true);
diff --git a/solr/core/src/java/org/apache/solr/search/MatchCostQuery.java 
b/solr/core/src/java/org/apache/solr/search/MatchCostQuery.java
new file mode 100644
index 0000000..2523941
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/MatchCostQuery.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.search;
+
+import java.io.IOException;
+import java.util.Objects;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.BulkScorer;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Matches;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
+import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.TwoPhaseIterator;
+import org.apache.lucene.search.Weight;
+
+/** Wraps a {@link Query} to customize the {@link 
TwoPhaseIterator#matchCost()}. */
+public class MatchCostQuery extends Query {
+  private final Query delegate;
+  private final float matchCost;
+
+  public MatchCostQuery(Query delegate, float matchCost) {
+    this.delegate = delegate;
+    this.matchCost = matchCost;
+    assert matchCost >= 0;
+  }
+
+  @Override
+  public String toString(String field) {
+    return delegate.toString(field);
+  }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    // don't visit our wrapper, just delegate
+    delegate.visit(visitor);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return sameClassAs(other) &&
+            Objects.equals(delegate, ((MatchCostQuery) other).delegate);
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * classHash() + delegate.hashCode();
+  }
+
+  @Override
+  public Query rewrite(IndexReader reader) throws IOException {
+    final Query rewrite = delegate.rewrite(reader);
+    if (delegate == rewrite) {
+      return this; // unchanged
+    }
+    return new MatchCostQuery(rewrite, matchCost);
+  }
+
+  @Override
+  public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, 
float boost) throws IOException {
+    return new Weight(this) {
+      final Weight weight = delegate.createWeight(searcher, scoreMode, boost);
+
+      @Override
+      public boolean isCacheable(LeafReaderContext ctx) {
+        return weight.isCacheable(ctx);
+      }
+
+      @Override
+      public Matches matches(LeafReaderContext context, int doc) throws 
IOException {
+        return weight.matches(context, doc);
+      }
+
+      @Override
+      public Explanation explain(LeafReaderContext context, int doc) throws 
IOException {
+        return weight.explain(context, doc);
+      }
+
+      // do not delegate scorerSupplier(); use the default implementation that 
calls our
+      // scorer() so that we can wrap TPI
+
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        final Scorer scorer = weight.scorer(context);
+        if (scorer == null) {
+          return null;
+        }
+        final TwoPhaseIterator tpi = scorer.twoPhaseIterator();
+        if (tpi == null || tpi.matchCost() == matchCost) {
+          return scorer; // needn't wrap/delegate
+        }
+        return new Scorer(weight) { // pass delegated weight
+
+          @Override
+          public TwoPhaseIterator twoPhaseIterator() {
+            return new TwoPhaseIterator(tpi.approximation()) {
+              @Override
+              public boolean matches() throws IOException {
+                return tpi.matches();
+              }
+
+              @Override
+              public float matchCost() {
+                return matchCost;
+              }
+            };
+          }
+
+          @Override
+          public DocIdSetIterator iterator() {
+            return scorer.iterator();
+          }
+
+          @Override
+          public float getMaxScore(int upTo) throws IOException {
+            return scorer.getMaxScore(upTo);
+          }
+
+          @Override
+          public float score() throws IOException {
+            return scorer.score();
+          }
+
+          @Override
+          public int docID() {
+            return scorer.docID();
+          }
+        };
+      }
+
+      // delegate because thus there's no need to care about TPI matchCost if 
called
+      @Override
+      public BulkScorer bulkScorer(LeafReaderContext context) throws 
IOException {
+        return weight.bulkScorer(context);
+      }
+    };
+
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/search/QParser.java 
b/solr/core/src/java/org/apache/solr/search/QParser.java
index 5ebb05e..c472f4c 100644
--- a/solr/core/src/java/org/apache/solr/search/QParser.java
+++ b/solr/core/src/java/org/apache/solr/search/QParser.java
@@ -180,8 +180,6 @@ public abstract class QParser {
             extendedQuery().setCache(false);
           } else if (CommonParams.TRUE.equals(cacheStr)) {
             extendedQuery().setCache(true);
-          } else if ("sep".equals(cacheStr)) {
-            extendedQuery().setCacheSep(true);
           }
         }
 
diff --git 
a/solr/core/src/java/org/apache/solr/search/SolrConstantScoreQuery.java 
b/solr/core/src/java/org/apache/solr/search/SolrConstantScoreQuery.java
index 82beecb..bc3b191 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrConstantScoreQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrConstantScoreQuery.java
@@ -65,15 +65,6 @@ public class SolrConstantScoreQuery extends Query implements 
ExtendedQuery {
   }
 
   @Override
-  public void setCacheSep(boolean cacheSep) {
-  }
-
-  @Override
-  public boolean getCacheSep() {
-    return false;
-  }
-
-  @Override
   public void setCost(int cost) {
     this.cost = cost;
   }
diff --git a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java 
b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
index e1c9c29..b7fc3bc 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
@@ -28,16 +28,13 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
-
 import com.codahale.metrics.Gauge;
 import com.google.common.collect.Iterables;
-
 import org.apache.lucene.document.Document;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.ExitableDirectoryReader;
@@ -51,6 +48,7 @@ import org.apache.lucene.index.StoredFieldVisitor;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.*;
 import org.apache.lucene.search.TotalHits.Relation;
 import org.apache.lucene.store.AlreadyClosedException;
@@ -167,7 +165,7 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
         UninvertingReader.wrap(reader, 
core.getLatestSchema().getUninversionMapper()),
         SolrQueryTimeoutImpl.getInstance());
   }
-  
+
   /**
    * Builds the necessary collector chain (via delegate wrapping) and executes 
the query against it. This method takes
    * into consideration both the explicitly provided collector and postFilter 
as well as any needed collector wrappers
@@ -809,49 +807,6 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
     return answerBits;
   }
 
-  /**
-   * Returns the set of document ids matching a query. This method is 
cache-aware and attempts to retrieve the answer
-   * from the cache if possible. If the answer was not cached, it may have 
been inserted into the cache as a result of
-   * this call. This method can handle negative queries.
-   * <p>
-   * The DocSet returned should <b>not</b> be modified.
-   */
-  public DocSet getDocSet(Query query) throws IOException {
-    if (query instanceof ExtendedQuery) {
-      ExtendedQuery eq = (ExtendedQuery) query;
-      if (!eq.getCache()) {
-        if (query instanceof WrappedQuery) {
-          query = ((WrappedQuery) query).getWrappedQuery();
-        }
-        query = QueryUtils.makeQueryable(query);
-        return getDocSetNC(query, null);
-      }
-    }
-
-    // Get the absolute value (positive version) of this query. If we
-    // get back the same reference, we know it's positive.
-    Query absQ = QueryUtils.getAbs(query);
-    boolean positive = query == absQ;
-
-    if (filterCache != null) {
-      DocSet absAnswer = filterCache.get(absQ);
-      if (absAnswer != null) {
-        if (positive) return absAnswer;
-        else return getLiveDocSet().andNot(absAnswer);
-      }
-    }
-
-    DocSet absAnswer = getDocSetNC(absQ, null);
-    DocSet answer = positive ? absAnswer : getLiveDocSet().andNot(absAnswer);
-
-    if (filterCache != null) {
-      // cache negative queries as positive
-      filterCache.put(absQ, absAnswer);
-    }
-
-    return answer;
-  }
-
   // only handle positive (non negative) queries
   DocSet getPositiveDocSet(Query q) throws IOException {
     DocSet answer;
@@ -910,7 +865,8 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
     this.liveDocs = makeBitDocSet(docs);
   }
 
-  private static Comparator<Query> sortByCost = (q1, q2) -> ((ExtendedQuery) 
q1).getCost() - ((ExtendedQuery) q2).getCost();
+  private static Comparator<ExtendedQuery> sortByCost =
+      Comparator.comparingInt(ExtendedQuery::getCost);
 
   /**
    * Returns the set of document ids matching all queries. This method is 
cache-aware and attempts to retrieve the
@@ -958,7 +914,7 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
    */
   public static class ProcessedFilter {
     public DocSet answer; // maybe null. Sometimes we have a docSet answer 
that represents the complete answer / result.
-    public Filter filter; // maybe null
+    public Query filter; // maybe null
     public DelegatingCollector postFilter; // maybe null
   }
 
@@ -978,13 +934,13 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
     }
 
     // We combine all the filter queries that come from the filter cache & 
setFilter into "answer".
-    // This might become pf.filterAsDocSet but not if there are any non-cached 
filters
+    // This might become pf.answer but not if there are any non-cached filters
     DocSet answer = null;
 
     boolean[] neg = new boolean[queries.size() + 1];
     DocSet[] sets = new DocSet[queries.size() + 1];
-    List<Query> notCached = null;
-    List<Query> postFilters = null;
+    List<ExtendedQuery> notCached = null;
+    List<PostFilter> postFilters = null;
 
     int end = 0;
     int smallestIndex = -1;
@@ -1001,10 +957,10 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
         if (!eq.getCache()) {
           if (eq.getCost() >= 100 && eq instanceof PostFilter) {
             if (postFilters == null) postFilters = new ArrayList<>(sets.length 
- end);
-            postFilters.add(q);
+            postFilters.add((PostFilter) q);
           } else {
             if (notCached == null) notCached = new ArrayList<>(sets.length - 
end);
-            notCached.add(q);
+            notCached.add((ExtendedQuery) q);
           }
           continue;
         }
@@ -1078,13 +1034,16 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
         pf.filter = answer.getTopFilter();
       }
     } else {
-      Collections.sort(notCached, sortByCost);
-      List<Weight> weights = new ArrayList<>(notCached.size());
-      for (Query q : notCached) {
-        Query qq = QueryUtils.makeQueryable(q);
-        weights.add(createWeight(rewrite(qq), ScoreMode.COMPLETE_NO_SCORES, 
1));
+      notCached.sort(sortByCost); // pointless?
+      final BooleanQuery.Builder builder = new BooleanQuery.Builder();
+      if (answer != null) {
+        builder.add(answer.getTopFilter(), Occur.FILTER);
       }
-      pf.filter = new FilterImpl(answer, weights);
+      for (ExtendedQuery eq : notCached) {
+        Query q = eq.getCostAppliedQuery();
+        builder.add(q, Occur.FILTER);
+      }
+      pf.filter = builder.build();
     }
 
     // Set pf.postFilter
@@ -1092,7 +1051,7 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
       Collections.sort(postFilters, sortByCost);
       for (int i = postFilters.size() - 1; i >= 0; i--) {
         DelegatingCollector prev = pf.postFilter;
-        pf.postFilter = ((PostFilter) 
postFilters.get(i)).getFilterCollector(this);
+        pf.postFilter = postFilters.get(i).getFilterCollector(this);
         if (prev != null) pf.postFilter.setDelegate(prev);
       }
     }
@@ -1187,44 +1146,58 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
   }
 
   /**
-   * Returns the set of document ids matching both the query and the filter. 
This method is cache-aware and attempts to
-   * retrieve the answer from the cache if possible. If the answer was not 
cached, it may have been inserted into the
-   * cache as a result of this call.
+   * Returns the set of document ids matching both the query.
+   * This method is cache-aware and attempts to retrieve a DocSet of the query 
from the cache if possible.
+   * If the answer was not cached, it may have been inserted into the cache as 
a result of this call.
    *
-   * @param filter
-   *          may be null
-   * @return DocSet meeting the specified criteria, should <b>not</b> be 
modified by the caller.
+   * @return Non-null DocSet meeting the specified criteria.  Should 
<b>not</b> be modified by the caller.
+   * @see #getDocSet(Query,DocSet)
    */
-  public DocSet getDocSet(Query query, DocSet filter) throws IOException {
-    if (filter == null) return getDocSet(query);
+  public DocSet getDocSet(Query query) throws IOException {
+    return getDocSet(query, null);
+  }
 
+  /**
+   * Returns the set of document ids matching both the query and the filter.
+   * This method is cache-aware and attempts to retrieve a DocSet of the query 
from the cache if possible.
+   * If the answer was not cached, it may have been inserted into the cache as 
a result of this call.
+   *
+   * @param filter may be null if none
+   * @return Non-null DocSet meeting the specified criteria.  Should 
<b>not</b> be modified by the caller.
+   */
+  public DocSet getDocSet(Query query, DocSet filter) throws IOException {
+    boolean doCache = filterCache != null;
     if (query instanceof ExtendedQuery) {
-      ExtendedQuery eq = (ExtendedQuery) query;
-      if (!eq.getCache()) {
-        if (query instanceof WrappedQuery) {
-          query = ((WrappedQuery) query).getWrappedQuery();
-        }
-        query = QueryUtils.makeQueryable(query);
-        return getDocSetNC(query, filter);
+      if (!((ExtendedQuery) query).getCache()) {
+        doCache = false;
+      }
+      if (query instanceof WrappedQuery) {
+        query = ((WrappedQuery) query).getWrappedQuery();
       }
     }
 
-    // Negative query if absolute value different from original
+    if (!doCache) {
+      query = QueryUtils.makeQueryable(query);
+      return getDocSetNC(query, filter);
+    }
+
+    // Get the absolute value (positive version) of this query. If we
+    // get back the same reference, we know it's positive.
     Query absQ = QueryUtils.getAbs(query);
     boolean positive = absQ == query;
 
-    DocSet first;
-    if (filterCache != null) {
-      first = filterCache.get(absQ);
-      if (first == null) {
-        first = getDocSetNC(absQ, null);
-        filterCache.put(absQ, first);
-      }
-      return positive ? first.intersection(filter) : filter.andNot(first);
+    // note: can't use computeIfAbsent because can be recursive
+    DocSet absAnswer = filterCache.get(absQ);
+    if (absAnswer == null) {
+      absAnswer = getDocSetNC(absQ, null);
+      filterCache.put(absQ, absAnswer);
     }
 
-    // If there isn't a cache, then do a single filtered query if positive.
-    return positive ? getDocSetNC(absQ, filter) : 
filter.andNot(getPositiveDocSet(absQ));
+    if (filter == null) {
+      return positive ? absAnswer : getLiveDocSet().andNot(absAnswer);
+    } else {
+      return positive ? absAnswer.intersection(filter) : 
filter.andNot(absAnswer);
+    }
   }
 
   /**
@@ -2086,8 +2059,8 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
       // have deleted documents, but UninvertedField's doNegative has sets 
with deleted docs
       TotalHitCountCollector collector = new TotalHitCountCollector();
       BooleanQuery.Builder bq = new BooleanQuery.Builder();
-      bq.add(QueryUtils.makeQueryable(a), BooleanClause.Occur.MUST);
-      bq.add(new ConstantScoreQuery(b.getTopFilter()), 
BooleanClause.Occur.MUST);
+      bq.add(QueryUtils.makeQueryable(a), Occur.MUST);
+      bq.add(new ConstantScoreQuery(b.getTopFilter()), Occur.MUST);
       super.search(bq.build(), collector);
       return collector.getTotalHits();
     }
@@ -2316,9 +2289,9 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
         }), true, "statsCache", Category.CACHE.toString(), scope);
   }
 
-  /** 
-   * wraps a gauge (related to an IndexReader) and swallows any {@link 
AlreadyClosedException} that 
-   * might be thrown, returning the specified default in it's place. 
+  /**
+   * wraps a gauge (related to an IndexReader) and swallows any {@link 
AlreadyClosedException} that
+   * might be thrown, returning the specified default in it's place.
    */
   private <T> Gauge<T> rgauge(T closedDefault, Gauge<T> g) {
     return () -> {
@@ -2329,178 +2302,7 @@ public class SolrIndexSearcher extends IndexSearcher 
implements Closeable, SolrI
       }
     };
   }
-  
-  private static class FilterImpl extends Filter {
-    private final Filter topFilter;
-    private final List<Weight> weights;
-
-    public FilterImpl(DocSet filter, List<Weight> weights) {
-      this.weights = weights;
-      this.topFilter = filter == null ? null : filter.getTopFilter();
-    }
 
-    @Override
-    public DocIdSet getDocIdSet(LeafReaderContext context, Bits acceptDocs) 
throws IOException {
-      final DocIdSet sub = topFilter == null ? null : 
topFilter.getDocIdSet(context, acceptDocs);
-      if (weights.size() == 0) return sub;
-      return new FilterSet(sub, context);
-    }
-
-    @Override
-    public String toString(String field) {
-      return "SolrFilter";
-    }
-
-    @Override
-    public void visit(QueryVisitor visitor) {
-      visitor.visitLeaf(this);
-    }
-
-    private class FilterSet extends DocIdSet {
-      private final DocIdSet docIdSet;
-      private final LeafReaderContext context;
-
-      public FilterSet(DocIdSet docIdSet, LeafReaderContext context) {
-        this.docIdSet = docIdSet;
-        this.context = context;
-      }
-
-      @Override
-      public DocIdSetIterator iterator() throws IOException {
-        List<DocIdSetIterator> iterators = new ArrayList<>(weights.size() + 1);
-        if (docIdSet != null) {
-          final DocIdSetIterator iter = docIdSet.iterator();
-          if (iter == null) return null;
-          iterators.add(iter);
-        }
-        for (Weight w : weights) {
-          final Scorer scorer = w.scorer(context);
-          if (scorer == null) return null;
-          iterators.add(scorer.iterator());
-        }
-        if (iterators.isEmpty()) return null;
-        if (iterators.size() == 1) return iterators.get(0);
-        if (iterators.size() == 2) return new 
DualFilterIterator(iterators.get(0), iterators.get(1));
-        return new FilterIterator(iterators.toArray(new 
DocIdSetIterator[iterators.size()]));
-      }
-
-      @Override
-      public Bits bits() throws IOException {
-        return null; // don't use random access
-      }
-
-      @Override
-      public long ramBytesUsed() {
-        return docIdSet != null ? docIdSet.ramBytesUsed() : 0L;
-      }
-    }
-
-    private static class FilterIterator extends DocIdSetIterator {
-      private final DocIdSetIterator[] iterators;
-      private final DocIdSetIterator first;
-
-      public FilterIterator(DocIdSetIterator[] iterators) {
-        this.iterators = iterators;
-        this.first = iterators[0];
-      }
-
-      @Override
-      public int docID() {
-        return first.docID();
-      }
-
-      private int advanceAllTo(int doc) throws IOException {
-        int highestDocIter = 0; // index of the iterator with the highest id
-        int i = 1; // We already advanced the first iterator before calling 
this method
-        while (i < iterators.length) {
-          if (i != highestDocIter) {
-            final int next = iterators[i].advance(doc);
-            if (next != doc) { // We need to advance all iterators to a new 
target
-              doc = next;
-              highestDocIter = i;
-              i = 0;
-              continue;
-            }
-          }
-          ++i;
-        }
-        return doc;
-      }
-
-      @Override
-      public int nextDoc() throws IOException {
-        return advanceAllTo(first.nextDoc());
-      }
-
-      @Override
-      public int advance(int target) throws IOException {
-        return advanceAllTo(first.advance(target));
-      }
-
-      @Override
-      public long cost() {
-        return first.cost();
-      }
-    }
-
-    private static class DualFilterIterator extends DocIdSetIterator {
-      private final DocIdSetIterator a;
-      private final DocIdSetIterator b;
-
-      public DualFilterIterator(DocIdSetIterator a, DocIdSetIterator b) {
-        this.a = a;
-        this.b = b;
-      }
-
-      @Override
-      public int docID() {
-        return a.docID();
-      }
-
-      @Override
-      public int nextDoc() throws IOException {
-        return doNext(a.nextDoc());
-      }
-
-      @Override
-      public int advance(int target) throws IOException {
-        return doNext(a.advance(target));
-      }
-
-      @Override
-      public long cost() {
-        return Math.min(a.cost(), b.cost());
-      }
-
-      private int doNext(int doc) throws IOException {
-        for (;;) {
-          int other = b.advance(doc);
-          if (other == doc) return doc;
-          doc = a.advance(other);
-          if (other == doc) return doc;
-        }
-      }
-
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      return sameClassAs(other) &&
-             equalsTo(getClass().cast(other));
-    }
-
-    private boolean equalsTo(FilterImpl other) {
-      return Objects.equals(this.topFilter, other.topFilter) &&
-             Objects.equals(this.weights, other.weights);
-    }
-
-    @Override
-    public int hashCode() {
-      return classHash() 
-          + 31 * Objects.hashCode(topFilter)
-          + 31 * Objects.hashCode(weights);
-    }
-  }
   public long getWarmupTime() {
     return warmupTime;
   }
diff --git a/solr/core/src/test/org/apache/solr/core/SOLR749Test.java 
b/solr/core/src/test/org/apache/solr/core/SOLR749Test.java
index d802899..9a8218f 100644
--- a/solr/core/src/test/org/apache/solr/core/SOLR749Test.java
+++ b/solr/core/src/test/org/apache/solr/core/SOLR749Test.java
@@ -25,7 +25,7 @@ import org.junit.BeforeClass;
 /**
  * This class started life as a test for SOLR-749 to prove that value source 
plugins were properly
  * intialized, but it has since evolved to also help prove that ValueSource's 
are not asked to compute
- * values for documents unneccessarily.
+ * values for documents unnecessarily.
  *
  * @see CountUsageValueSourceParser
  * @see <a href="https://issues.apache.org/jira/browse/SOLR-749";>SOLR-749</a>
@@ -116,6 +116,17 @@ public class SOLR749Test extends SolrTestCaseJ4 {
               "//result[@numFound=0]");
       assertEquals(0, 
CountUsageValueSourceParser.getAndClearCount("postfilt_match_all"));
 
+      // Tests that TwoPhaseIterator is employed optimally
+      // note: map(countUsage(lowCost,0),0,0,id_i1) == return "id_i1" value & 
keep track of access
+      assertQ("query matching 20 -> 10 -> 5 docs; two non-cached queries",
+          req("q","{!notfoo cache=false}id_i1:[20 TO 39]", // match 20
+              // the below IDs have alternating even/odd pairings so as to 
test possible sequencing of evaluation
+              "fq","{!notfoo cache=false}id_i1:(20 21 25 26 28 29 31 32 36 
37)", // match 10 (subset of above)
+              "fq","{!frange cache=false cost=5 l=21 u=99 
v='map(countUsage(lowCost,0),0,0,id_i1)'})", // eliminate #20
+              "fq","{!frange cache=false cost=10 l=1 
v='mod(map(countUsage(lastFilter,0),0,0,id_i1),2)'}"), // match 5 -- (the odd 
ones since l=1 thus don't match 0)
+          "//result[@numFound=5]");
+      assertEquals(10, 
CountUsageValueSourceParser.getAndClearCount("lowCost"));
+      assertEquals(9, 
CountUsageValueSourceParser.getAndClearCount("lastFilter"));
     } finally {
       CountUsageValueSourceParser.clearCounters();
     }
diff --git 
a/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java 
b/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
index 58b1901..735a14f 100644
--- a/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
+++ b/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
@@ -420,15 +420,6 @@ public class SolrIndexSearcherTest extends SolrTestCaseJ4 {
     public void setCost(int cost) {}
 
     @Override
-    public boolean getCacheSep() {
-      return false;
-    }
-
-    @Override
-    public void setCacheSep(boolean cacheSep) {
-    }
-
-    @Override
     public DelegatingCollector getFilterCollector(IndexSearcher searcher) {
       return new DelegatingCollector() {
         private int collected = 0;

Reply via email to