You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ro...@apache.org on 2019/03/14 15:04:37 UTC

[lucene-solr] branch master updated: LUCENE-3041: QueryVisitor (#581)

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

romseygeek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/master by this push:
     new fbd0516  LUCENE-3041: QueryVisitor (#581)
fbd0516 is described below

commit fbd05167f455e3ce2b2ead50336e2b9c2521cd6c
Author: Alan Woodward <ro...@gmail.com>
AuthorDate: Thu Mar 14 15:04:33 2019 +0000

    LUCENE-3041: QueryVisitor (#581)
    
    This commit adds an introspection API to Query, allowing users to traverse
    the nested structure of a query and examine its leaves.  It replaces the existing
    `extractTerms` method on Weight, and alters some highlighting code to use
    the new API
---
 lucene/CHANGES.txt                                 |  13 +
 .../classification/utils/NearestFuzzyQuery.java    |   6 +
 .../org/apache/lucene/document/FeatureQuery.java   |  22 +-
 .../lucene/document/LatLonDocValuesBoxQuery.java   |   8 +
 .../document/LatLonDocValuesDistanceQuery.java     |   8 +
 .../document/LatLonPointDistanceFeatureQuery.java  |  13 +-
 .../lucene/document/LatLonPointDistanceQuery.java  |   8 +
 .../lucene/document/LatLonPointInPolygonQuery.java |   8 +
 .../lucene/document/LongDistanceFeatureQuery.java  |  13 +-
 .../apache/lucene/document/RangeFieldQuery.java    |  10 +-
 .../document/SortedNumericDocValuesRangeQuery.java |   8 +
 .../document/SortedSetDocValuesRangeQuery.java     |   8 +
 .../org/apache/lucene/search/AutomatonQuery.java   |   9 +-
 .../org/apache/lucene/search/BlendedTermQuery.java |  11 +-
 .../org/apache/lucene/search/BooleanQuery.java     |  12 +
 .../org/apache/lucene/search/BooleanWeight.java    |  13 -
 .../java/org/apache/lucene/search/BoostQuery.java  |   5 +
 .../apache/lucene/search/ConstantScoreQuery.java   |   5 +
 .../apache/lucene/search/ConstantScoreWeight.java  |   9 -
 .../apache/lucene/search/DisjunctionMaxQuery.java  |  19 +-
 .../lucene/search/DocValuesFieldExistsQuery.java   |   7 +
 .../lucene/search/DocValuesRewriteMethod.java      |   7 +
 .../org/apache/lucene/search/FilterWeight.java     |   7 -
 .../java/org/apache/lucene/search/FuzzyQuery.java  |  20 +-
 .../org/apache/lucene/search/FuzzyTermsEnum.java   |  62 +++-
 .../lucene/search/IndexOrDocValuesQuery.java       |  14 +-
 .../org/apache/lucene/search/LRUQueryCache.java    |   6 -
 .../apache/lucene/search/MatchAllDocsQuery.java    |   5 +
 .../org/apache/lucene/search/MatchNoDocsQuery.java |  11 +-
 .../org/apache/lucene/search/MultiPhraseQuery.java |  21 +-
 .../search/MultiTermQueryConstantScoreWrapper.java |   7 +
 .../org/apache/lucene/search/NGramPhraseQuery.java |   5 +
 .../lucene/search/NormsFieldExistsQuery.java       |   7 +
 .../java/org/apache/lucene/search/PhraseQuery.java |  19 +-
 .../org/apache/lucene/search/PointInSetQuery.java  |   7 +
 .../org/apache/lucene/search/PointRangeQuery.java  |   7 +
 .../src/java/org/apache/lucene/search/Query.java   |   6 +
 .../org/apache/lucene/search/QueryVisitor.java     |  94 ++++++
 .../org/apache/lucene/search/SynonymQuery.java     |  25 +-
 .../org/apache/lucene/search/TermInSetQuery.java   |  25 +-
 .../java/org/apache/lucene/search/TermQuery.java   |  13 +-
 .../src/java/org/apache/lucene/search/Weight.java  |  10 -
 .../org/apache/lucene/search/package-info.java     |   6 -
 .../lucene/search/spans/FieldMaskingSpanQuery.java |  15 +-
 .../apache/lucene/search/spans/SpanBoostQuery.java |   9 +
 .../lucene/search/spans/SpanContainQuery.java      |  31 +-
 .../search/spans/SpanMultiTermQueryWrapper.java    |  10 +-
 .../apache/lucene/search/spans/SpanNearQuery.java  |  31 +-
 .../apache/lucene/search/spans/SpanNotQuery.java   |  19 +-
 .../apache/lucene/search/spans/SpanOrQuery.java    |  21 +-
 .../search/spans/SpanPositionCheckQuery.java       |  15 +-
 .../apache/lucene/search/spans/SpanTermQuery.java  |  16 +-
 .../apache/lucene/document/TestFeatureField.java   |  26 --
 .../apache/lucene/search/JustCompileSearch.java    |  12 +-
 .../lucene/search/TestBoolean2ScorerSupplier.java  |  10 +-
 .../org/apache/lucene/search/TestBooleanQuery.java |  45 ++-
 .../apache/lucene/search/TestBooleanScorer.java    |  11 +-
 .../apache/lucene/search/TestConjunctionDISI.java  |   7 -
 .../lucene/search/TestConstantScoreQuery.java      |  22 +-
 .../apache/lucene/search/TestLRUQueryCache.java    |  31 +-
 .../lucene/search/TestMaxScoreSumPropagator.java   |   7 -
 .../lucene/search/TestMultiTermQueryRewrites.java  |   5 +
 .../org/apache/lucene/search/TestNeedsScores.java  |   5 +
 .../org/apache/lucene/search/TestPrefixRandom.java |   5 +
 .../apache/lucene/search/TestQueryRescorer.java    |  10 +-
 .../org/apache/lucene/search/TestQueryVisitor.java | 333 +++++++++++++++++++++
 .../apache/lucene/search/TestRegexpRandom2.java    |   7 +-
 .../org/apache/lucene/search/TestScorerPerf.java   |   7 +-
 .../org/apache/lucene/search/TestSortRandom.java   |   5 +
 .../TestUsageTrackingFilterCachingPolicy.java      |   5 +
 .../org/apache/lucene/search/TestWANDScorer.java   |   5 +
 .../search/spans/JustCompileSearchSpans.java       |   6 +
 .../search/spans/TestFieldMaskingSpanQuery.java    |  13 +-
 .../org/apache/lucene/facet/DrillDownQuery.java    |   6 +
 .../apache/lucene/facet/DrillSidewaysQuery.java    |  11 +-
 .../org/apache/lucene/facet/range/DoubleRange.java |   6 +
 .../org/apache/lucene/facet/range/LongRange.java   |   6 +
 .../org/apache/lucene/facet/TestDrillSideways.java |   6 +
 .../lucene/facet/range/TestRangeFacetCounts.java   |   6 +
 .../search/highlight/QueryTermExtractor.java       |  90 +++---
 .../highlight/WeightedSpanTermExtractor.java       |   7 +-
 .../uhighlight/MemoryIndexOffsetStrategy.java      |  13 +-
 .../search/uhighlight/MultiTermHighlighting.java   | 236 +++++++--------
 .../lucene/search/uhighlight/OffsetsEnum.java      |  27 +-
 .../lucene/search/uhighlight/PhraseHelper.java     |  33 +-
 .../search/uhighlight/UnifiedHighlighter.java      |  23 +-
 .../lucene/search/highlight/HighlighterTest.java   |   6 +
 .../highlight/custom/HighlightCustomQueryTest.java |  16 +-
 .../uhighlight/TestUnifiedHighlighterMTQ.java      |  17 +-
 .../TestUnifiedHighlighterStrictPhrases.java       |   6 +
 .../search/vectorhighlight/FieldQueryTest.java     |   8 +
 .../lucene/search/join/GlobalOrdinalsQuery.java    |  10 +-
 .../search/join/GlobalOrdinalsWithScoreQuery.java  |  10 +-
 .../search/join/ParentChildrenBlockJoinQuery.java  |  13 +-
 .../search/join/PointInSetIncludingScoreQuery.java |  14 +-
 .../search/join/TermsIncludingScoreQuery.java      |  13 +-
 .../org/apache/lucene/search/join/TermsQuery.java  |   6 +
 .../lucene/search/join/ToChildBlockJoinQuery.java  |   9 +-
 .../lucene/search/join/ToParentBlockJoinQuery.java |   6 +
 .../apache/lucene/search/join/TestBlockJoin.java   |  33 +-
 .../apache/lucene/search/join/TestJoinUtil.java    |  16 +-
 .../search/TestDiversifiedTopDocsCollector.java    |  13 +-
 .../apache/lucene/queries/CommonTermsQuery.java    |  12 +-
 .../queries/function/FunctionMatchQuery.java       |   6 +
 .../lucene/queries/function/FunctionQuery.java     |  12 +-
 .../queries/function/FunctionRangeQuery.java       |  13 +-
 .../queries/function/FunctionScoreQuery.java       |  14 +-
 .../lucene/queries/mlt/MoreLikeThisQuery.java      |  18 +-
 .../lucene/queries/payloads/PayloadScoreQuery.java |  13 +-
 .../queries/payloads/SpanPayloadCheckQuery.java    |  16 +-
 .../complexPhrase/ComplexPhraseQueryParser.java    |   6 +
 .../surround/query/DistanceRewriteQuery.java       |   8 +
 .../surround/query/SimpleTermRewriteQuery.java     |  14 +-
 .../queryparser/classic/TestMultiAnalyzer.java     |  17 +-
 .../apache/lucene/document/LatLonShapeQuery.java   |   8 +
 .../lucene/sandbox/queries/FuzzyLikeThisQuery.java |   8 +-
 .../java/org/apache/lucene/search/BM25FQuery.java  |  14 +-
 .../org/apache/lucene/search/CoveringQuery.java    |  17 +-
 .../lucene/search/DocValuesNumbersQuery.java       |   8 +
 .../apache/lucene/search/DocValuesTermsQuery.java  |   9 +-
 .../apache/lucene/search/TermAutomatonQuery.java   |  23 +-
 .../intervals/ConjunctionIntervalsSource.java      |  10 +-
 .../intervals/DifferenceIntervalsSource.java       |  10 +-
 .../intervals/DisjunctionIntervalsSource.java      |  11 +-
 .../search/intervals/ExtendedIntervalsSource.java  |   7 +-
 .../search/intervals/FilteredIntervalsSource.java  |   7 +-
 .../intervals/FixedFieldIntervalsSource.java       |   7 +-
 .../lucene/search/intervals/IntervalQuery.java     |  15 +-
 .../lucene/search/intervals/IntervalsSource.java   |   9 +-
 .../MinimumShouldMatchIntervalsSource.java         |  10 +-
 .../search/intervals/MultiTermIntervalsSource.java |   5 +-
 .../search/intervals/OffsetIntervalsSource.java    |   7 +-
 .../search/intervals/TermIntervalsSource.java      |   6 +-
 .../sandbox/queries/FuzzyLikeThisQueryTest.java    |  16 +-
 .../lucene/search/TestTermAutomatonQuery.java      |   5 +
 .../spatial/composite/CompositeVerifyQuery.java    |   6 +
 .../composite/IntersectsRPTVerifyQuery.java        |   6 +
 .../spatial/prefix/AbstractPrefixTreeQuery.java    |   8 +
 .../spatial/serialized/SerializedDVStrategy.java   |   6 +
 .../lucene/spatial/vector/PointVectorStrategy.java |   6 +
 .../lucene/spatial3d/PointInGeo3DShapeQuery.java   |   8 +
 .../search/suggest/document/CompletionWeight.java  |   7 -
 .../search/suggest/document/ContextQuery.java      |   6 +
 .../suggest/document/PrefixCompletionQuery.java    |   5 +
 .../suggest/document/RegexCompletionQuery.java     |   6 +
 .../org/apache/lucene/search/AssertingQuery.java   |   5 +
 .../lucene/search/BlockScoreQueryWrapper.java      |  12 +-
 .../java/org/apache/lucene/search/QueryUtils.java  |   3 +
 .../lucene/search/RandomApproximationQuery.java    |   7 +-
 .../lucene/search/ShardSearchingTestBase.java      |   5 +-
 .../lucene/search/spans/AssertingSpanQuery.java    |  12 +-
 .../lucene/search/spans/AssertingSpanWeight.java   |   7 -
 .../java/org/apache/solr/ltr/LTRScoringQuery.java  |  15 +-
 .../java/org/apache/solr/ltr/feature/Feature.java  |  13 +-
 .../solr/ltr/feature/OriginalScoreFeature.java     |   7 -
 .../org/apache/solr/ltr/feature/SolrFeature.java   |   9 -
 .../solr/legacy/LegacyNumericRangeQuery.java       |   8 +-
 .../apache/solr/parser/SolrQueryParserBase.java    |   8 +-
 .../java/org/apache/solr/query/FilterQuery.java    |   5 +
 .../java/org/apache/solr/query/SolrRangeQuery.java |   8 +-
 .../java/org/apache/solr/schema/LatLonType.java    |   7 +
 .../apache/solr/search/AbstractReRankQuery.java    |   6 +
 .../org/apache/solr/search/AnalyticsQuery.java     |   8 +-
 .../solr/search/CollapsingQParserPlugin.java       |   6 +
 .../apache/solr/search/ExportQParserPlugin.java    |   6 +
 .../src/java/org/apache/solr/search/Filter.java    |  11 +-
 .../solr/search/GraphTermsQParserPlugin.java       |  20 +-
 .../org/apache/solr/search/HashQParserPlugin.java  |   6 +
 .../org/apache/solr/search/JoinQParserPlugin.java  |   6 +
 .../apache/solr/search/SolrConstantScoreQuery.java |   6 +
 .../org/apache/solr/search/SolrIndexSearcher.java  |   5 +
 .../java/org/apache/solr/search/WrappedQuery.java  |  10 +-
 .../solr/search/join/BlockJoinFacetFilter.java     |   6 +
 .../org/apache/solr/search/join/GraphQuery.java    |  14 +-
 .../solr/search/join/ScoreJoinQParserPlugin.java   |   6 +
 .../apache/solr/search/stats/ExactStatsCache.java  |  34 ++-
 .../apache/solr/update/DeleteByQueryWrapper.java   |  13 +-
 .../org/apache/solr/update/SolrIndexSplitter.java  |   6 +
 .../apache/solr/search/RankQueryTestPlugin.java    |   6 +
 .../solr/uninverting/TestFieldCacheSortRandom.java |   8 +-
 180 files changed, 1835 insertions(+), 913 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index cf9f100..203dbc8 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -10,6 +10,9 @@ API Changes
 * LUCENE-8474: RAMDirectory and associated deprecated classes have been 
   removed. (Dawid Weiss)
 
+* LUCENE-3041: The deprecated Weight#extractTerms() method has been 
+  removed (Alan Woodward, Simon Willnauer, David Smiley, Luca Cavanna)
+
 Bug fixes:
 
 * LUCENE-8663: NRTCachingDirectory.slowFileExists may open a file while 
@@ -17,6 +20,16 @@ Bug fixes:
 
 ======================= Lucene 8.1.0 =======================
 
+API Changes
+
+* LUCENE-3041: A query introspection API has been added.  Queries should
+  implement a visit() method, taking a QueryVisitor, and either pass the
+  visitor down to any child queries, or call a visitX() or consumeX() method
+  on it.  All locations in the code that called Weight.extractTerms() 
+  have been changed to use this API, and the extractTerms() method has 
+  been deprecated. (Alan Woodward, Simon Willnauer, David Smiley, Luca
+  Cavanna)
+
 Bug fixes
 
 * LUCENE-8712: Polygon2D does not detect crossings through segment edges.
diff --git a/lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java b/lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java
index ad15372..3b80de7 100644
--- a/lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java
+++ b/lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java
@@ -39,6 +39,7 @@ import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.FuzzyTermsEnum;
 import org.apache.lucene.search.MaxNonCompetitiveBoostAttribute;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.util.AttributeSource;
 import org.apache.lucene.util.BytesRef;
@@ -327,4 +328,9 @@ public class NearestFuzzyQuery extends Query {
         Objects.equals(fieldVals, other.fieldVals);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
 }
diff --git a/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java b/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java
index efc2d00..bc69344 100644
--- a/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java
@@ -18,14 +18,12 @@ package org.apache.lucene.document;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.document.FeatureField.FeatureFunction;
 import org.apache.lucene.index.ImpactsEnum;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.DocIdSetIterator;
@@ -33,6 +31,7 @@ import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.ImpactsDISI;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -90,18 +89,6 @@ final class FeatureQuery extends Query {
       }
 
       @Override
-      public void extractTerms(Set<Term> terms) {
-        if (scoreMode.needsScores() == false) {
-          // features are irrelevant to highlighting, skip
-        } else {
-          // extracting the term here will help get better scoring with
-          // distributed term statistics if the saturation function is used
-          // and the pivot value is computed automatically
-          terms.add(new Term(fieldName, featureName));
-        }
-      }
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         String desc = "weight(" + getQuery() + " in " + doc + ") [" + function + "]";
 
@@ -175,6 +162,13 @@ final class FeatureQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(fieldName)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public String toString(String field) {
     return "FeatureQuery(field=" + fieldName + ", feature=" + featureName + ", function=" + function + ")";
   }
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesBoxQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesBoxQuery.java
index f34089ef..180caa5 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesBoxQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesBoxQuery.java
@@ -27,6 +27,7 @@ import org.apache.lucene.search.ConstantScoreScorer;
 import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -96,6 +97,13 @@ final class LatLonDocValuesBoxQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new ConstantScoreWeight(this, boost) {
       @Override
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java
index 5fd1924..5ea137b 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java
@@ -27,6 +27,7 @@ import org.apache.lucene.search.ConstantScoreScorer;
 import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -55,6 +56,13 @@ final class LatLonDocValuesDistanceQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder sb = new StringBuilder();
     if (!this.field.equals(field)) {
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceFeatureQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceFeatureQuery.java
index 3f30dea..3a34cae 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceFeatureQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceFeatureQuery.java
@@ -18,7 +18,6 @@ package org.apache.lucene.document;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.geo.GeoEncodingUtils;
 import org.apache.lucene.geo.GeoUtils;
@@ -30,11 +29,11 @@ import org.apache.lucene.index.PointValues;
 import org.apache.lucene.index.PointValues.IntersectVisitor;
 import org.apache.lucene.index.PointValues.Relation;
 import org.apache.lucene.index.SortedNumericDocValues;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.ScorerSupplier;
@@ -65,6 +64,13 @@ final class LatLonPointDistanceFeatureQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public final boolean equals(Object o) {
     return sameClassAs(o) &&
         equalsTo(getClass().cast(o));
@@ -102,9 +108,6 @@ final class LatLonPointDistanceFeatureQuery extends Query {
       }
 
       @Override
-      public void extractTerms(Set<Term> terms) {}
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         SortedNumericDocValues multiDocValues = DocValues.getSortedNumeric(context.reader(), field);
         if (multiDocValues.advanceExact(doc) == false) {
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java
index ca9ff22..969a133 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java
@@ -32,6 +32,7 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.ScorerSupplier;
@@ -72,6 +73,13 @@ final class LatLonPointDistanceQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     Rectangle box = Rectangle.fromPointDistance(latitude, longitude, radiusMeters);
     // create bounding box(es) for the distance range
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
index 2e4d98d..e425d16 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
@@ -33,6 +33,7 @@ import org.apache.lucene.search.ConstantScoreScorer;
 import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -76,6 +77,13 @@ final class LatLonPointInPolygonQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
 
     // I don't use RandomAccessWeight here: it's no good to approximate with "match all docs"; this is an inverted structure and should be
diff --git a/lucene/core/src/java/org/apache/lucene/document/LongDistanceFeatureQuery.java b/lucene/core/src/java/org/apache/lucene/document/LongDistanceFeatureQuery.java
index 480cfce..a2b793b 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LongDistanceFeatureQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LongDistanceFeatureQuery.java
@@ -18,7 +18,6 @@ package org.apache.lucene.document;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.LeafReaderContext;
@@ -27,11 +26,11 @@ import org.apache.lucene.index.PointValues;
 import org.apache.lucene.index.PointValues.IntersectVisitor;
 import org.apache.lucene.index.PointValues.Relation;
 import org.apache.lucene.index.SortedNumericDocValues;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.ScorerSupplier;
@@ -76,6 +75,13 @@ final class LongDistanceFeatureQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public String toString(String field) {
     return getClass().getSimpleName() + "(field=" + field + ",origin=" + origin + ",pivotDistance=" + pivotDistance + ")";
   }
@@ -90,9 +96,6 @@ final class LongDistanceFeatureQuery extends Query {
       }
 
       @Override
-      public void extractTerms(Set<Term> terms) {}
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         SortedNumericDocValues multiDocValues = DocValues.getSortedNumeric(context.reader(), field);
         if (multiDocValues.advanceExact(doc) == false) {
diff --git a/lucene/core/src/java/org/apache/lucene/document/RangeFieldQuery.java b/lucene/core/src/java/org/apache/lucene/document/RangeFieldQuery.java
index 6b235fe..ac21a43 100644
--- a/lucene/core/src/java/org/apache/lucene/document/RangeFieldQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/RangeFieldQuery.java
@@ -31,10 +31,11 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.ScorerSupplier;
 import org.apache.lucene.search.Weight;
-import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.util.DocIdSetBuilder;
 import org.apache.lucene.util.FutureArrays;
 
@@ -262,6 +263,13 @@ abstract class RangeFieldQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new ConstantScoreWeight(this, boost) {
 
diff --git a/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java b/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java
index 771af5c..60222c3 100644
--- a/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java
@@ -30,6 +30,7 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.DocValuesFieldExistsQuery;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -68,6 +69,13 @@ abstract class SortedNumericDocValuesRangeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder b = new StringBuilder();
     if (this.field.equals(field) == false) {
diff --git a/lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java b/lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java
index e5d365f..88792ba 100644
--- a/lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/document/SortedSetDocValuesRangeQuery.java
@@ -30,6 +30,7 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.DocValuesFieldExistsQuery;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -79,6 +80,13 @@ abstract class SortedSetDocValuesRangeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder b = new StringBuilder();
     if (this.field.equals(field) == false) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java b/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java
index 7fb155d..a4a5344 100644
--- a/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java
@@ -151,7 +151,14 @@ public class AutomatonQuery extends MultiTermQuery {
     buffer.append("}");
     return buffer.toString();
   }
-  
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField())) {
+      visitor.visitLeaf(this);
+    }
+  }
+
   /** Returns the automaton used to create this query */
   public Automaton getAutomaton() {
     return automaton;
diff --git a/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java b/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java
index 8f85e25..00ba5a1 100644
--- a/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java
@@ -25,8 +25,8 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.TermStates;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.InPlaceMergeSorter;
@@ -294,6 +294,15 @@ public final class BlendedTermQuery extends Query {
     return rewriteMethod.rewrite(termQueries);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    Term[] termsToVisit = Arrays.stream(terms).filter(t -> visitor.acceptField(t.field())).toArray(Term[]::new);
+    if (termsToVisit.length > 0) {
+      QueryVisitor v = visitor.getSubVisitor(Occur.SHOULD, this);
+      v.consumeTerms(this, termsToVisit);
+    }
+  }
+
   private static TermStates adjustFrequencies(IndexReaderContext readerContext,
                                               TermStates ctx, int artificialDf, long artificialTtf) throws IOException {
     List<LeafReaderContext> leaves = readerContext.leaves();
diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java
index f52df9f..87105b0 100644
--- a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java
@@ -451,6 +451,18 @@ public class BooleanQuery extends Query implements Iterable<BooleanClause> {
     return super.rewrite(reader);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    for (BooleanClause.Occur occur : clauseSets.keySet()) {
+      if (clauseSets.get(occur).size() > 0) {
+        QueryVisitor v = visitor.getSubVisitor(occur, this);
+        for (Query q : clauseSets.get(occur)) {
+          q.visit(v);
+        }
+      }
+    }
+  }
+
   /** Prints a user-readable version of this query. */
   @Override
   public String toString(String field) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanWeight.java b/lucene/core/src/java/org/apache/lucene/search/BooleanWeight.java
index 6298513..cded651 100644
--- a/lucene/core/src/java/org/apache/lucene/search/BooleanWeight.java
+++ b/lucene/core/src/java/org/apache/lucene/search/BooleanWeight.java
@@ -24,10 +24,8 @@ import java.util.EnumMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.similarities.Similarity;
 import org.apache.lucene.util.Bits;
@@ -57,17 +55,6 @@ final class BooleanWeight extends Weight {
   }
 
   @Override
-  public void extractTerms(Set<Term> terms) {
-    int i = 0;
-    for (BooleanClause clause : query) {
-      if (clause.isScoring() || (scoreMode.needsScores() == false && clause.isProhibited() == false)) {
-        weights.get(i).extractTerms(terms);
-      }
-      i++;
-    }
-  }
-
-  @Override
   public Explanation explain(LeafReaderContext context, int doc) throws IOException {
     final int minShouldMatch = query.getMinimumNumberShouldMatch();
     List<Explanation> subs = new ArrayList<>();
diff --git a/lucene/core/src/java/org/apache/lucene/search/BoostQuery.java b/lucene/core/src/java/org/apache/lucene/search/BoostQuery.java
index 4e4649c..5f04363 100644
--- a/lucene/core/src/java/org/apache/lucene/search/BoostQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/BoostQuery.java
@@ -105,6 +105,11 @@ public final class BoostQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    query.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder builder = new StringBuilder();
     builder.append("(");
diff --git a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreQuery.java b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreQuery.java
index 5c9ed19..9ad256d 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreQuery.java
@@ -64,6 +64,11 @@ public final class ConstantScoreQuery extends Query {
     return super.rewrite(reader);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    query.visit(visitor.getSubVisitor(BooleanClause.Occur.FILTER, this));
+  }
+
   /** We return this as our {@link BulkScorer} so that if the CSQ
    *  wraps a query with its own optimized top-level
    *  scorer (e.g. BooleanScorer) we can use that
diff --git a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreWeight.java b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreWeight.java
index 671ec71..7ee4cce 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ConstantScoreWeight.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ConstantScoreWeight.java
@@ -18,10 +18,8 @@ package org.apache.lucene.search;
 
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 
 /**
  * A Weight that has a constant score equal to the boost of the wrapped query.
@@ -39,13 +37,6 @@ public abstract class ConstantScoreWeight extends Weight {
     this.score = score;
   }
 
-  @Override
-  public void extractTerms(Set<Term> terms) {
-    // most constant-score queries don't wrap index terms
-    // eg. geo filters, doc values queries, ...
-    // override if your constant-score query does wrap terms
-  }
-
   /** Return the score produced by this {@link Weight}. */
   protected final float score() {
     return score;
diff --git a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java
index 43b42b5..4ffd326 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java
@@ -24,11 +24,9 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
-import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.index.LeafReaderContext;
 
 /**
  * A query that generates the union of documents produced by its subqueries, and that scores each document with the maximum
@@ -112,13 +110,6 @@ public final class DisjunctionMaxQuery extends Query implements Iterable<Query>
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      for (Weight weight : weights) {
-        weight.extractTerms(terms);
-      }
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       List<Matches> mis = new ArrayList<>();
       for (Weight weight : weights) {
@@ -237,6 +228,14 @@ public final class DisjunctionMaxQuery extends Query implements Iterable<Query>
     return super.rewrite(reader);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+    for (Query q : disjuncts) {
+      q.visit(v);
+    }
+  }
+
   /** Prettyprint us.
    * @param field the field to which we are applied
    * @return a string that shows what we do, of the form "(disjunct1 | disjunct2 | ... | disjunctn)^boost"
diff --git a/lucene/core/src/java/org/apache/lucene/search/DocValuesFieldExistsQuery.java b/lucene/core/src/java/org/apache/lucene/search/DocValuesFieldExistsQuery.java
index 4b9c97d..bca34c2 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DocValuesFieldExistsQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DocValuesFieldExistsQuery.java
@@ -60,6 +60,13 @@ public final class DocValuesFieldExistsQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) {
     return new ConstantScoreWeight(this, boost) {
       @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/DocValuesRewriteMethod.java b/lucene/core/src/java/org/apache/lucene/search/DocValuesRewriteMethod.java
index d328b42..b1a7ef7 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DocValuesRewriteMethod.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DocValuesRewriteMethod.java
@@ -70,6 +70,13 @@ public final class DocValuesRewriteMethod extends MultiTermQuery.RewriteMethod {
     
     /** Returns the field name for this query */
     public final String getField() { return query.getField(); }
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+      if (visitor.acceptField(query.getField())) {
+        visitor.visitLeaf(this);
+      }
+    }
     
     @Override
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
diff --git a/lucene/core/src/java/org/apache/lucene/search/FilterWeight.java b/lucene/core/src/java/org/apache/lucene/search/FilterWeight.java
index 8a2b57b..5292e78 100644
--- a/lucene/core/src/java/org/apache/lucene/search/FilterWeight.java
+++ b/lucene/core/src/java/org/apache/lucene/search/FilterWeight.java
@@ -17,10 +17,8 @@
 package org.apache.lucene.search;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 
 /**
  * A {@code FilterWeight} contains another {@code Weight} and implements
@@ -61,11 +59,6 @@ public abstract class FilterWeight extends Weight {
   }
 
   @Override
-  public void extractTerms(Set<Term> terms) {
-    in.extractTerms(terms);
-  }
-
-  @Override
   public Explanation explain(LeafReaderContext context, int doc) throws IOException {
     return in.explain(context, doc);
   }
diff --git a/lucene/core/src/java/org/apache/lucene/search/FuzzyQuery.java b/lucene/core/src/java/org/apache/lucene/search/FuzzyQuery.java
index 3c1eacd..344f921 100644
--- a/lucene/core/src/java/org/apache/lucene/search/FuzzyQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/FuzzyQuery.java
@@ -24,6 +24,7 @@ import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.util.AttributeSource;
+import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.LevenshteinAutomata;
 
 /** Implements the fuzzy search query. The similarity measurement
@@ -146,6 +147,21 @@ public class FuzzyQuery extends MultiTermQuery {
     return transpositions;
   }
 
+  /**
+   * Expert: Constructs an equivalent Automaton accepting terms matched by this query
+   */
+  public Automaton toAutomaton() {
+    return FuzzyTermsEnum.buildAutomaton(term.text(), prefixLength, transpositions, maxEdits);
+  }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    // TODO find some way of consuming Automata
+    if (visitor.acceptField(term.field())) {
+      visitor.visitLeaf(this);
+    }
+  }
+
   @Override
   protected TermsEnum getTermsEnum(Terms terms, AttributeSource atts) throws IOException {
     if (maxEdits == 0 || prefixLength >= term.text().length()) {  // can only match if it's exact
@@ -153,7 +169,7 @@ public class FuzzyQuery extends MultiTermQuery {
     }
     return new FuzzyTermsEnum(terms, atts, getTerm(), maxEdits, prefixLength, transpositions);
   }
-  
+
   /**
    * Returns the pattern term.
    */
@@ -173,7 +189,7 @@ public class FuzzyQuery extends MultiTermQuery {
     buffer.append(Integer.toString(maxEdits));
     return buffer.toString();
   }
-  
+
   @Override
   public int hashCode() {
     final int prime = 31;
diff --git a/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java b/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java
index a6d56e7..4759220 100644
--- a/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java
+++ b/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java
@@ -17,6 +17,9 @@
 package org.apache.lucene.search;
 
 
+import java.io.IOException;
+import java.util.Arrays;
+
 import org.apache.lucene.index.BaseTermsEnum;
 import org.apache.lucene.index.ImpactsEnum;
 import org.apache.lucene.index.PostingsEnum;
@@ -35,9 +38,6 @@ import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.CompiledAutomaton;
 import org.apache.lucene.util.automaton.LevenshteinAutomata;
 
-import java.io.IOException;
-import java.util.Arrays;
-
 /** Subclass of TermsEnum for enumerating all terms that are similar
  * to the specified filter term.
  *
@@ -111,11 +111,7 @@ public final class FuzzyTermsEnum extends BaseTermsEnum {
     this.term = term;
     
     // convert the string into a utf32 int[] representation for fast comparisons
-    final String utf16 = term.text();
-    this.termText = new int[utf16.codePointCount(0, utf16.length())];
-    for (int cp, i = 0, j = 0; i < utf16.length(); i += Character.charCount(cp)) {
-      termText[j++] = cp = utf16.codePointAt(i);
-    }
+    this.termText = stringToUTF32(term.text());
     this.termLength = termText.length;
 
     this.dfaAtt = atts.addAttribute(LevenshteinAutomataAttribute.class);
@@ -133,16 +129,10 @@ public final class FuzzyTermsEnum extends BaseTermsEnum {
     CompiledAutomaton[] prevAutomata = dfaAtt.automata();
     if (prevAutomata == null) {
       prevAutomata = new CompiledAutomaton[maxEdits+1];
-
-      LevenshteinAutomata builder = 
-        new LevenshteinAutomata(UnicodeUtil.newString(termText, realPrefixLength, termText.length - realPrefixLength), transpositions);
-
-      String prefix = UnicodeUtil.newString(termText, 0, realPrefixLength);
+      Automaton[] automata = buildAutomata(termText, prefixLength, transpositions, maxEdits);
       for (int i = 0; i <= maxEdits; i++) {
-        Automaton a = builder.toAutomaton(i, prefix);
-        prevAutomata[i] = new CompiledAutomaton(a, true, false);
+        prevAutomata[i] = new CompiledAutomaton(automata[i], true, false);
       }
-
       // first segment computes the automata, and we share with subsequent segments via this Attribute:
       dfaAtt.setAutomata(prevAutomata);
     }
@@ -152,6 +142,46 @@ public final class FuzzyTermsEnum extends BaseTermsEnum {
     bottomTerm = maxBoostAtt.getCompetitiveTerm();
     bottomChanged(null);
   }
+
+  /**
+   * Builds a binary Automaton to match a fuzzy term
+   * @param text            the term to match
+   * @param prefixLength    length of a required common prefix
+   * @param transpositions  {@code true} if transpositions should count as a single edit
+   * @param maxEdits        the maximum edit distance of matching terms
+   */
+  public static Automaton buildAutomaton(String text, int prefixLength, boolean transpositions, int maxEdits) {
+    int[] termText = stringToUTF32(text);
+    Automaton[] automata = buildAutomata(termText, prefixLength, transpositions, maxEdits);
+    return automata[automata.length - 1];
+  }
+
+  private static int[] stringToUTF32(String text) {
+    int[] termText = new int[text.codePointCount(0, text.length())];
+    for (int cp, i = 0, j = 0; i < text.length(); i += Character.charCount(cp)) {
+      termText[j++] = cp = text.codePointAt(i);
+    }
+    return termText;
+  }
+
+  private static Automaton[] buildAutomata(int[] termText, int prefixLength, boolean transpositions, int maxEdits) {
+    if (maxEdits < 0 || maxEdits > LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE) {
+      throw new IllegalArgumentException("max edits must be 0.." + LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE + ", inclusive; got: " + maxEdits);
+    }
+    if (prefixLength < 0) {
+      throw new IllegalArgumentException("prefixLength cannot be less than 0");
+    }
+    Automaton[] automata = new Automaton[maxEdits + 1];
+    int termLength = termText.length;
+    prefixLength = Math.min(prefixLength, termLength);
+    String suffix = UnicodeUtil.newString(termText, prefixLength, termText.length - prefixLength);
+    LevenshteinAutomata builder = new LevenshteinAutomata(suffix, transpositions);
+    String prefix = UnicodeUtil.newString(termText, 0, prefixLength);
+    for (int i = 0; i <= maxEdits; i++) {
+      automata[i] = builder.toAutomaton(i, prefix);
+    }
+    return automata;
+  }
   
   /**
    * return an automata-based enum for matching up to editDistance from
diff --git a/lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java b/lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java
index d69421e..a3a3a22 100644
--- a/lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java
@@ -17,13 +17,11 @@
 package org.apache.lucene.search;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.document.LongPoint;
 import org.apache.lucene.document.SortedNumericDocValuesField;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 
 /**
  * A query that uses either an index structure (points or terms) or doc values
@@ -110,16 +108,18 @@ public final class IndexOrDocValuesQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.MUST, this);
+    indexQuery.visit(v);
+    dvQuery.visit(v);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     final Weight indexWeight = indexQuery.createWeight(searcher, scoreMode, boost);
     final Weight dvWeight = dvQuery.createWeight(searcher, scoreMode, boost);
     return new Weight(this) {
       @Override
-      public void extractTerms(Set<Term> terms) {
-        indexWeight.extractTerms(terms);
-      }
-
-      @Override
       public Matches matches(LeafReaderContext context, int doc) throws IOException {
         // We need to check a single doc, so the dv query should perform better
         return dvWeight.matches(context, doc);
diff --git a/lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java b/lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java
index c2448ed..ae62c57 100644
--- a/lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java
+++ b/lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java
@@ -36,7 +36,6 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.ReaderUtil;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TieredMergePolicy;
 import org.apache.lucene.util.Accountable;
 import org.apache.lucene.util.Accountables;
@@ -674,11 +673,6 @@ public class LRUQueryCache implements QueryCache, Accountable {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      in.extractTerms(terms);
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       return in.matches(context, doc);
     }
diff --git a/lucene/core/src/java/org/apache/lucene/search/MatchAllDocsQuery.java b/lucene/core/src/java/org/apache/lucene/search/MatchAllDocsQuery.java
index 80e8d32..ca414c2 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MatchAllDocsQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MatchAllDocsQuery.java
@@ -89,4 +89,9 @@ public final class MatchAllDocsQuery extends Query {
   public int hashCode() {
     return classHash();
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
diff --git a/lucene/core/src/java/org/apache/lucene/search/MatchNoDocsQuery.java b/lucene/core/src/java/org/apache/lucene/search/MatchNoDocsQuery.java
index 525a183..fae20a8 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MatchNoDocsQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MatchNoDocsQuery.java
@@ -18,10 +18,8 @@ package org.apache.lucene.search;
 
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 
 /**
  * A query that matches no documents.
@@ -45,10 +43,6 @@ public class MatchNoDocsQuery extends Query {
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new Weight(this) {
       @Override
-      public void extractTerms(Set<Term> terms) {
-      }
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         return Explanation.noMatch(reason);
       }
@@ -66,6 +60,11 @@ public class MatchNoDocsQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public String toString(String field) {
     return "MatchNoDocsQuery(\"" + reason + "\")";
   }
diff --git a/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java b/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java
index c8d22ba..4aafa7b 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java
@@ -21,12 +21,10 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexReaderContext;
@@ -204,6 +202,18 @@ public class MultiPhraseQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.MUST, this);
+    for (Term[] terms : termArrays) {
+      QueryVisitor sv = v.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+      sv.consumeTerms(this, terms);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     final Map<Term,TermStates> termStates = new HashMap<>();
     return new PhraseWeight(this, field, searcher, scoreMode) {
@@ -298,13 +308,6 @@ public class MultiPhraseQuery extends Query {
         }
 
       }
-
-      @Override
-      public void extractTerms(Set<Term> terms) {
-        for (final Term[] arr : termArrays) {
-          Collections.addAll(terms, arr);
-        }
-      }
     };
   }
 
diff --git a/lucene/core/src/java/org/apache/lucene/search/MultiTermQueryConstantScoreWrapper.java b/lucene/core/src/java/org/apache/lucene/search/MultiTermQueryConstantScoreWrapper.java
index 9c37211..2d7ac9b 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MultiTermQueryConstantScoreWrapper.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MultiTermQueryConstantScoreWrapper.java
@@ -231,4 +231,11 @@ final class MultiTermQueryConstantScoreWrapper<Q extends MultiTermQuery> extends
 
     };
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField())) {
+      query.visit(visitor.getSubVisitor(Occur.FILTER, this));
+    }
+  }
 }
diff --git a/lucene/core/src/java/org/apache/lucene/search/NGramPhraseQuery.java b/lucene/core/src/java/org/apache/lucene/search/NGramPhraseQuery.java
index db997d3..3f86afa 100644
--- a/lucene/core/src/java/org/apache/lucene/search/NGramPhraseQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/NGramPhraseQuery.java
@@ -78,6 +78,11 @@ public class NGramPhraseQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    phraseQuery.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+  }
+
+  @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
            equalsTo(getClass().cast(other));
diff --git a/lucene/core/src/java/org/apache/lucene/search/NormsFieldExistsQuery.java b/lucene/core/src/java/org/apache/lucene/search/NormsFieldExistsQuery.java
index 8c86885..984a9a8 100644
--- a/lucene/core/src/java/org/apache/lucene/search/NormsFieldExistsQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/NormsFieldExistsQuery.java
@@ -62,6 +62,13 @@ public final class NormsFieldExistsQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new ConstantScoreWeight(this, boost) {
       @Override
diff --git a/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java b/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java
index 8f04271..271fed6 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java
@@ -20,9 +20,7 @@ package org.apache.lucene.search;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 import org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat;
 import org.apache.lucene.codecs.lucene50.Lucene50PostingsReader;
@@ -255,6 +253,9 @@ public class PhraseQuery extends Query {
    */
   public int getSlop() { return slop; }
 
+  /** Returns the field this query applies to */
+  public String getField() { return field; }
+
   /** Returns the list of terms in this phrase. */
   public Term[] getTerms() {
     return terms;
@@ -284,6 +285,15 @@ public class PhraseQuery extends Query {
     }
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.MUST, this);
+    v.consumeTerms(this, terms);
+  }
+
   static class PostingsAndFreq implements Comparable<PostingsAndFreq> {
     final PostingsEnum postings;
     final int position;
@@ -460,11 +470,6 @@ public class PhraseQuery extends Query {
           return new SloppyPhraseMatcher(postingsFreqs, slop, totalMatchCost, exposeOffsets);
         }
       }
-
-      @Override
-      public void extractTerms(Set<Term> queryTerms) {
-        Collections.addAll(queryTerms, terms);
-      }
     };
   }
 
diff --git a/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java b/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java
index b6d047e..ad9e258 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PointInSetQuery.java
@@ -106,6 +106,13 @@ public abstract class PointInSetQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
 
     // We don't use RandomAccessWeight here: it's no good to approximate with "match all docs".
diff --git a/lucene/core/src/java/org/apache/lucene/search/PointRangeQuery.java b/lucene/core/src/java/org/apache/lucene/search/PointRangeQuery.java
index ca61d51..57c8708 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PointRangeQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PointRangeQuery.java
@@ -100,6 +100,13 @@ public abstract class PointRangeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
 
     // We don't use RandomAccessWeight here: it's no good to approximate with "match all docs".
diff --git a/lucene/core/src/java/org/apache/lucene/search/Query.java b/lucene/core/src/java/org/apache/lucene/search/Query.java
index 54de63f..c594ff0 100644
--- a/lucene/core/src/java/org/apache/lucene/search/Query.java
+++ b/lucene/core/src/java/org/apache/lucene/search/Query.java
@@ -75,6 +75,12 @@ public abstract class Query {
   }
 
   /**
+   * Recurse through the query tree, visiting any child queries
+   * @param visitor a QueryVisitor to be called by each query in the tree
+   */
+  public abstract void visit(QueryVisitor visitor);
+
+  /**
    * Override and implement query instance equivalence properly in a subclass. 
    * This is required so that {@link QueryCache} works properly.
    * 
diff --git a/lucene/core/src/java/org/apache/lucene/search/QueryVisitor.java b/lucene/core/src/java/org/apache/lucene/search/QueryVisitor.java
new file mode 100644
index 0000000..5635f7d
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/search/QueryVisitor.java
@@ -0,0 +1,94 @@
+/*
+ * 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.lucene.search;
+
+import java.util.Arrays;
+import java.util.Set;
+
+import org.apache.lucene.index.Term;
+
+/**
+ * Allows recursion through a query tree
+ *
+ * @see Query#visit(QueryVisitor)
+ */
+public abstract class QueryVisitor {
+
+  /**
+   * Called by leaf queries that match on specific terms
+   *
+   * @param query  the leaf query
+   * @param terms  the terms the query will match on
+   */
+  public void consumeTerms(Query query, Term... terms) { }
+
+  // TODO it would be nice to have a way to consume 'classes' of Terms from
+  // things like AutomatonQuery
+
+  /**
+   * Called by leaf queries that do not match on terms
+   * @param query the query
+   */
+  public void visitLeaf(Query query) { }
+
+  /**
+   * Whether or not terms from this field are of interest to the visitor
+   *
+   * Implement this to avoid collecting terms from heavy queries such as {@link TermInSetQuery}
+   * that are not running on fields of interest
+   */
+  public boolean acceptField(String field) {
+    return true;
+  }
+
+  /**
+   * Pulls a visitor instance for visiting child clauses of a query
+   *
+   * The default implementation returns {@code this}, unless {@code occur} is equal
+   * to {@link BooleanClause.Occur#MUST_NOT} in which case it returns
+   * {@link #EMPTY_VISITOR}
+   *
+   * @param occur   the relationship between the parent and its children
+   * @param parent  the query visited
+   */
+  public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
+    if (occur == BooleanClause.Occur.MUST_NOT) {
+      return EMPTY_VISITOR;
+    }
+    return this;
+  }
+
+  /**
+   * Builds a {@code QueryVisitor} instance that collects all terms that may match a query
+   * @param termSet a {@code Set} to add collected terms to
+   */
+  public static QueryVisitor termCollector(Set<Term> termSet) {
+    return new QueryVisitor() {
+      @Override
+      public void consumeTerms(Query query, Term... terms) {
+        termSet.addAll(Arrays.asList(terms));
+      }
+    };
+  }
+
+  /**
+   * A QueryVisitor implementation that does nothing
+   */
+  public static final QueryVisitor EMPTY_VISITOR = new QueryVisitor() {};
+
+}
diff --git a/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java b/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java
index fc3a5e4..2d053b6 100644
--- a/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java
@@ -25,7 +25,6 @@ import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.apache.lucene.index.Impact;
@@ -54,7 +53,9 @@ import org.apache.lucene.util.PriorityQueue;
  * term frequencies for the document.
  */
 public final class SynonymQuery extends Query {
+
   private final TermAndBoost terms[];
+  private final String field;
 
   /**
    * A builder for {@link SynonymQuery}.
@@ -102,7 +103,7 @@ public final class SynonymQuery extends Query {
      */
     public SynonymQuery build() {
       Collections.sort(terms, Comparator.comparing(a -> a.term));
-      return new SynonymQuery(terms.toArray(new TermAndBoost[0]));
+      return new SynonymQuery(terms.toArray(new TermAndBoost[0]), field);
     }
   }
   
@@ -111,8 +112,9 @@ public final class SynonymQuery extends Query {
    * <p>
    * The terms must all have the same field.
    */
-  private SynonymQuery(TermAndBoost[] terms) {
+  private SynonymQuery(TermAndBoost[] terms, String field) {
     this.terms = Objects.requireNonNull(terms);
+    this.field = field;
   }
 
   public List<Term> getTerms() {
@@ -165,6 +167,16 @@ public final class SynonymQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+    Term[] ts = Arrays.stream(terms).map(t -> t.term).toArray(Term[]::new);
+    v.consumeTerms(this, ts);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     if (scoreMode.needsScores()) {
       return new SynonymWeight(this, searcher, scoreMode, boost);
@@ -210,13 +222,6 @@ public final class SynonymQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      for (TermAndBoost term : SynonymQuery.this.terms) {
-        terms.add(term.term);
-      }
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       String field = terms[0].term.field();
       Terms indexTerms = context.reader().terms(field);
diff --git a/lucene/core/src/java/org/apache/lucene/search/TermInSetQuery.java b/lucene/core/src/java/org/apache/lucene/search/TermInSetQuery.java
index 814bea2..35298a7 100644
--- a/lucene/core/src/java/org/apache/lucene/search/TermInSetQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/TermInSetQuery.java
@@ -23,7 +23,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.SortedSet;
 
 import org.apache.lucene.index.IndexReader;
@@ -33,8 +32,8 @@ import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.PrefixCodedTerms;
 import org.apache.lucene.index.PrefixCodedTerms.TermIterator;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.BooleanClause.Occur;
@@ -123,6 +122,20 @@ public class TermInSetQuery extends Query implements Accountable {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(Occur.SHOULD, this);
+    List<Term> terms = new ArrayList<>();
+    TermIterator iterator = termData.iterator();
+    for (BytesRef term = iterator.next(); term != null; term = iterator.next()) {
+      terms.add(new Term(field, BytesRef.deepCopyOf(term)));
+    }
+    v.consumeTerms(this, terms.toArray(new Term[0]));
+  }
+
+  @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
         equalsTo(getClass().cast(other));
@@ -213,14 +226,6 @@ public class TermInSetQuery extends Query implements Accountable {
     return new ConstantScoreWeight(this, boost) {
 
       @Override
-      public void extractTerms(Set<Term> terms) {
-        // no-op
-        // This query is for abuse cases when the number of terms is too high to
-        // run efficiently as a BooleanQuery. So likewise we hide its terms in
-        // order to protect highlighters
-      }
-
-      @Override
       public Matches matches(LeafReaderContext context, int doc) throws IOException {
         Terms terms = context.reader().terms(field);
         if (terms == null || terms.hasPositions() == false) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/TermQuery.java b/lucene/core/src/java/org/apache/lucene/search/TermQuery.java
index 1eebbce..945c1b3 100644
--- a/lucene/core/src/java/org/apache/lucene/search/TermQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/TermQuery.java
@@ -19,7 +19,6 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReader;
@@ -76,11 +75,6 @@ public class TermQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      terms.add(getTerm());
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       TermsEnum te = getTermsEnum(context);
       if (te == null) {
@@ -205,6 +199,13 @@ public class TermQuery extends Query {
     return new TermWeight(searcher, scoreMode, boost, termState);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(term.field())) {
+      visitor.consumeTerms(this, term);
+    }
+  }
+
   /** Prints a user-readable version of this query. */
   @Override
   public String toString(String field) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/Weight.java b/lucene/core/src/java/org/apache/lucene/search/Weight.java
index d2fac4c..2f86f4f 100644
--- a/lucene/core/src/java/org/apache/lucene/search/Weight.java
+++ b/lucene/core/src/java/org/apache/lucene/search/Weight.java
@@ -18,12 +18,10 @@ package org.apache.lucene.search;
 
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.util.Bits;
 
 /**
@@ -62,14 +60,6 @@ public abstract class Weight implements SegmentCacheable {
   }
 
   /**
-   * Expert: adds all terms occurring in this query to the terms set. If the
-   * {@link Weight} was created with {@code needsScores == true} then this
-   * method will only extract terms which are used for scoring, otherwise it
-   * will extract all terms which are used for matching.
-   */
-  public abstract void extractTerms(Set<Term> terms);
-
-  /**
    * Returns {@link Matches} for a specific document, or {@code null} if the document
    * does not match the parent query
    *
diff --git a/lucene/core/src/java/org/apache/lucene/search/package-info.java b/lucene/core/src/java/org/apache/lucene/search/package-info.java
index 8120840..fdcd6d6 100644
--- a/lucene/core/src/java/org/apache/lucene/search/package-info.java
+++ b/lucene/core/src/java/org/apache/lucene/search/package-info.java
@@ -398,12 +398,6 @@
  *                 {@link org.apache.lucene.search.similarities.Similarity.SimScorer#explain(Explanation, long) SimScorer#explain(Explanation freq, long norm)}.
  *             </li>
  *             <li>
- *                 {@link org.apache.lucene.search.Weight#extractTerms(java.util.Set) extractTerms(Set&lt;Term&gt; terms)} &mdash; Extract terms that
- *                 this query operates on. This is typically used to support distributed search: knowing the terms that a query operates on helps
- *                 merge index statistics of these terms so that scores are computed over a subset of the data like they would if all documents
- *                 were in the same index.
- *             </li>
- *             <li>
  *                 {@link org.apache.lucene.search.Weight#matches matches(LeafReaderContext context, int doc)} &mdash; Give information about positions
  *                 and offsets of matches. This is typically useful to implement highlighting.
  *             </li>
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/FieldMaskingSpanQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/FieldMaskingSpanQuery.java
index 4a4c4fb..118fc06 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/FieldMaskingSpanQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/FieldMaskingSpanQuery.java
@@ -17,14 +17,16 @@
 package org.apache.lucene.search.spans;
 
 
+import java.io.IOException;
+import java.util.Objects;
+
 import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 
-import java.io.IOException;
-import java.util.Objects;
-
 /**
  * <p>Wrapper to allow {@link SpanQuery} objects participate in composite 
  * single-field SpanQueries by 'lying' about their search field. That is, 
@@ -105,6 +107,13 @@ public final class FieldMaskingSpanQuery extends SpanQuery {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      maskedQuery.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+    }
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder buffer = new StringBuilder();
     buffer.append("mask(");
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanBoostQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanBoostQuery.java
index 9556959..fec5792 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanBoostQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanBoostQuery.java
@@ -21,9 +21,11 @@ import java.io.IOException;
 import java.util.Objects;
 
 import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 
 /**
@@ -94,6 +96,13 @@ public final class SpanBoostQuery extends SpanQuery {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField())) {
+      query.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+    }
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder builder = new StringBuilder();
     builder.append("(");
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanContainQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanContainQuery.java
index 23c1e2b..2df1f0b 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanContainQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanContainQuery.java
@@ -17,18 +17,19 @@
 package org.apache.lucene.search.spans;
 
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Objects;
+
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
+import org.apache.lucene.search.QueryVisitor;
 
 abstract class SpanContainQuery extends SpanQuery implements Cloneable {
 
@@ -68,15 +69,6 @@ abstract class SpanContainQuery extends SpanQuery implements Cloneable {
       this.littleWeight = littleWeight;
     }
 
-    /**
-     * Extract terms from both <code>big</code> and <code>little</code>.
-     */
-    @Override
-    public void extractTerms(Set<Term> terms) {
-      bigWeight.extractTerms(terms);
-      littleWeight.extractTerms(terms);
-    }
-
     ArrayList<Spans> prepareConjunction(final LeafReaderContext context, Postings postings) throws IOException {
       Spans bigSpans = bigWeight.getSpans(context, postings);
       if (bigSpans == null) {
@@ -129,6 +121,15 @@ abstract class SpanContainQuery extends SpanQuery implements Cloneable {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField())) {
+      QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.MUST, this);
+      big.visit(v);
+      little.visit(v);
+    }
+  }
+
+  @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
            equalsTo(getClass().cast(other));
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanMultiTermQueryWrapper.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanMultiTermQueryWrapper.java
index 088e730..4f692ae 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanMultiTermQueryWrapper.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanMultiTermQueryWrapper.java
@@ -29,6 +29,7 @@ import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.ScoringRewrite;
 import org.apache.lucene.search.TopTermsRewrite;
@@ -121,7 +122,14 @@ public class SpanMultiTermQueryWrapper<Q extends MultiTermQuery> extends SpanQue
   public Query rewrite(IndexReader reader) throws IOException {
     return rewriteMethod.rewrite(reader, query);
   }
-  
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(query.getField())) {
+      query.visit(visitor.getSubVisitor(Occur.MUST, this));
+    }
+  }
+
   @Override
   public int hashCode() {
     return classHash() * 31 + query.hashCode();
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanNearQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanNearQuery.java
index 17b9e51..2f219bf 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanNearQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanNearQuery.java
@@ -24,15 +24,16 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.Terms;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 
@@ -226,13 +227,6 @@ public class SpanNearQuery extends SpanQuery implements Cloneable {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      for (SpanWeight w : subWeights) {
-        w.extractTerms(terms);
-      }
-    }
-
-    @Override
     public boolean isCacheable(LeafReaderContext ctx) {
       for (Weight w : subWeights) {
         if (w.isCacheable(ctx) == false)
@@ -266,6 +260,17 @@ public class SpanNearQuery extends SpanQuery implements Cloneable {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField()) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.MUST, this);
+    for (SpanQuery clause : clauses) {
+      clause.visit(v);
+    }
+  }
+
+  @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
            equalsTo(getClass().cast(other));
@@ -302,6 +307,11 @@ public class SpanNearQuery extends SpanQuery implements Cloneable {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public String toString(String field) {
       return "SpanGap(" + field + ":" + width + ")";
     }
@@ -328,11 +338,6 @@ public class SpanNearQuery extends SpanQuery implements Cloneable {
       }
 
       @Override
-      public void extractTerms(Set<Term> terms) {
-
-      }
-
-      @Override
       public boolean isCacheable(LeafReaderContext ctx) {
         return true;
       }
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanNotQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanNotQuery.java
index 6c56df3..5d998fe 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanNotQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanNotQuery.java
@@ -20,15 +20,16 @@ package org.apache.lucene.search.spans;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.TwoPhaseIterator;
 
@@ -189,11 +190,6 @@ public final class SpanNotQuery extends SpanQuery {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      includeWeight.extractTerms(terms);
-    }
-
-    @Override
     public boolean isCacheable(LeafReaderContext ctx) {
       return includeWeight.isCacheable(ctx) && excludeWeight.isCacheable(ctx);
     }
@@ -209,7 +205,16 @@ public final class SpanNotQuery extends SpanQuery {
     }
     return super.rewrite(reader);
   }
-    /** Returns true iff <code>o</code> is equal to this. */
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField())) {
+      include.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+      exclude.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST_NOT, this));
+    }
+  }
+
+  /** Returns true iff <code>o</code> is equal to this. */
   @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanOrQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanOrQuery.java
index 849edaa..5f589e1 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanOrQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanOrQuery.java
@@ -22,17 +22,18 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.DisiPriorityQueue;
 import org.apache.lucene.search.DisiWrapper;
 import org.apache.lucene.search.DisjunctionDISIApproximation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.TwoPhaseIterator;
 import org.apache.lucene.search.Weight;
@@ -89,6 +90,17 @@ public final class SpanOrQuery extends SpanQuery {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField()) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+    for (SpanQuery q : clauses) {
+      q.visit(v);
+    }
+  }
+
+  @Override
   public String toString(String field) {
     StringBuilder buffer = new StringBuilder();
     buffer.append("spanOr([");
@@ -134,13 +146,6 @@ public final class SpanOrQuery extends SpanQuery {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      for (final SpanWeight w: subWeights) {
-        w.extractTerms(terms);
-      }
-    }
-
-    @Override
     public boolean isCacheable(LeafReaderContext ctx) {
       for (Weight w : subWeights) {
         if (w.isCacheable(ctx) == false)
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanPositionCheckQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanPositionCheckQuery.java
index 099b627..3e40c64 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanPositionCheckQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanPositionCheckQuery.java
@@ -20,14 +20,15 @@ package org.apache.lucene.search.spans;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.spans.FilterSpans.AcceptStatus;
 
@@ -83,11 +84,6 @@ public abstract class SpanPositionCheckQuery extends SpanQuery implements Clonea
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      matchWeight.extractTerms(terms);
-    }
-
-    @Override
     public boolean isCacheable(LeafReaderContext ctx) {
       return matchWeight.isCacheable(ctx);
     }
@@ -126,6 +122,13 @@ public abstract class SpanPositionCheckQuery extends SpanQuery implements Clonea
     return super.rewrite(reader);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(getField())) {
+      match.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+    }
+  }
+
   /** Returns true iff <code>other</code> is equal to this. */
   @Override
   public boolean equals(Object other) {
diff --git a/lucene/core/src/java/org/apache/lucene/search/spans/SpanTermQuery.java b/lucene/core/src/java/org/apache/lucene/search/spans/SpanTermQuery.java
index 42e73f2..c86e7b7 100644
--- a/lucene/core/src/java/org/apache/lucene/search/spans/SpanTermQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/spans/SpanTermQuery.java
@@ -21,18 +21,18 @@ import java.io.IOException;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.ReaderUtil;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 
 /** Matches spans containing a term.
@@ -84,6 +84,13 @@ public class SpanTermQuery extends SpanQuery {
     return new SpanTermWeight(context, searcher, scoreMode.needsScores() ? Collections.singletonMap(term, context) : null, boost);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(term.field())) {
+      visitor.consumeTerms(this, term);
+    }
+  }
+
   public class SpanTermWeight extends SpanWeight {
 
     final TermStates termStates;
@@ -95,11 +102,6 @@ public class SpanTermQuery extends SpanQuery {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      terms.add(term);
-    }
-
-    @Override
     public boolean isCacheable(LeafReaderContext ctx) {
       return true;
     }
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestFeatureField.java b/lucene/core/src/test/org/apache/lucene/document/TestFeatureField.java
index 6a0a335..79534c8 100644
--- a/lucene/core/src/test/org/apache/lucene/document/TestFeatureField.java
+++ b/lucene/core/src/test/org/apache/lucene/document/TestFeatureField.java
@@ -17,15 +17,10 @@
 package org.apache.lucene.document;
 
 import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
 
 import org.apache.lucene.document.Field.Store;
 import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.MultiReader;
 import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
@@ -272,27 +267,6 @@ public class TestFeatureField extends LuceneTestCase {
     dir.close();
   }
 
-  public void testExtractTerms() throws IOException {
-    IndexReader reader = new MultiReader();
-    IndexSearcher searcher = newSearcher(reader);
-    Query query = FeatureField.newLogQuery("field", "term", 2f, 42);
-
-    Weight weight = searcher.createWeight(query, ScoreMode.COMPLETE_NO_SCORES, 1f);
-    Set<Term> terms = new HashSet<>();
-    weight.extractTerms(terms);
-    assertEquals(Collections.emptySet(), terms);
-
-    terms = new HashSet<>();
-    weight = searcher.createWeight(query, ScoreMode.COMPLETE, 1f);
-    weight.extractTerms(terms);
-    assertEquals(Collections.singleton(new Term("field", "term")), terms);
-
-    terms = new HashSet<>();
-    weight = searcher.createWeight(query, ScoreMode.TOP_SCORES, 1f);
-    weight.extractTerms(terms);
-    assertEquals(Collections.singleton(new Term("field", "term")), terms);
-  }
-
   public void testDemo() throws IOException {
     Directory dir = newDirectory();
     RandomIndexWriter writer = new RandomIndexWriter(random(), dir, newIndexWriterConfig()
diff --git a/lucene/core/src/test/org/apache/lucene/search/JustCompileSearch.java b/lucene/core/src/test/org/apache/lucene/search/JustCompileSearch.java
index a9d84cf..1029326 100644
--- a/lucene/core/src/test/org/apache/lucene/search/JustCompileSearch.java
+++ b/lucene/core/src/test/org/apache/lucene/search/JustCompileSearch.java
@@ -18,11 +18,9 @@ package org.apache.lucene.search;
 
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.FieldInvertState;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.similarities.Similarity;
 import org.apache.lucene.util.PriorityQueue;
 
@@ -154,6 +152,11 @@ final class JustCompileSearch {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      throw new UnsupportedOperationException(UNSUPPORTED_MSG);
+    }
+
+    @Override
     public boolean equals(Object obj) {
       throw new UnsupportedOperationException(UNSUPPORTED_MSG);
     }
@@ -243,11 +246,6 @@ final class JustCompileSearch {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      throw new UnsupportedOperationException(UNSUPPORTED_MSG);
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) {
       throw new UnsupportedOperationException(UNSUPPORTED_MSG);
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java b/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java
index ea2fd4c..45555f2 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestBoolean2ScorerSupplier.java
@@ -22,16 +22,13 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.EnumMap;
 import java.util.Map;
-import java.util.Set;
 
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
 
-import com.carrotsearch.randomizedtesting.generators.RandomPicks;
-
 public class TestBoolean2ScorerSupplier extends LuceneTestCase {
 
   private static class FakeWeight extends Weight {
@@ -41,11 +38,6 @@ public class TestBoolean2ScorerSupplier extends LuceneTestCase {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       return null;
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java
index 1b5da1b..99f36ac 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanQuery.java
@@ -22,14 +22,13 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.document.Document;
@@ -52,8 +51,6 @@ import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.NamedThreadFactory;
 import org.apache.lucene.util.TestUtil;
 
-import com.carrotsearch.randomizedtesting.generators.RandomPicks;
-
 public class TestBooleanQuery extends LuceneTestCase {
 
   public void testEquality() throws Exception {
@@ -754,7 +751,9 @@ public class TestBooleanQuery extends LuceneTestCase {
     assertEquals("a +b -c #d", bq.build().toString("field"));
   }
 
-  public void testExtractTerms() throws IOException {
+
+
+  public void testQueryVisitor() throws IOException {
     Term a = new Term("f", "a");
     Term b = new Term("f", "b");
     Term c = new Term("f", "c");
@@ -764,15 +763,37 @@ public class TestBooleanQuery extends LuceneTestCase {
     bqBuilder.add(new TermQuery(b), Occur.MUST);
     bqBuilder.add(new TermQuery(c), Occur.FILTER);
     bqBuilder.add(new TermQuery(d), Occur.MUST_NOT);
-    IndexSearcher searcher = new IndexSearcher(new MultiReader());
     BooleanQuery bq = bqBuilder.build();
 
-    Set<Term> scoringTerms = new HashSet<>();
-    searcher.createWeight(searcher.rewrite(bq), ScoreMode.COMPLETE, 1).extractTerms(scoringTerms);
-    assertEquals(new HashSet<>(Arrays.asList(a, b)), scoringTerms);
+    bq.visit(new QueryVisitor() {
+
+      Term expected;
 
-    Set<Term> matchingTerms = new HashSet<>();
-    searcher.createWeight(searcher.rewrite(bq), ScoreMode.COMPLETE_NO_SCORES, 1).extractTerms(matchingTerms);
-    assertEquals(new HashSet<>(Arrays.asList(a, b, c)), matchingTerms);
+      @Override
+      public QueryVisitor getSubVisitor(Occur occur, Query parent) {
+        switch (occur) {
+          case SHOULD:
+            expected = a;
+            break;
+          case MUST:
+            expected = b;
+            break;
+          case FILTER:
+            expected = c;
+            break;
+          case MUST_NOT:
+            expected = d;
+            break;
+          default:
+            throw new IllegalStateException();
+        }
+        return this;
+      }
+
+      @Override
+      public void consumeTerms(Query query, Term... terms) {
+        assertEquals(expected, terms[0]);
+      }
+    });
   }
 }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java
index 75fdd01..9e4351b 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java
@@ -19,7 +19,6 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.Set;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
@@ -80,11 +79,6 @@ public class TestBooleanScorer extends LuceneTestCase {
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
       return new Weight(CrazyMustUseBulkScorerQuery.this) {
         @Override
-        public void extractTerms(Set<Term> terms) {
-          throw new UnsupportedOperationException();
-        }
-
-        @Override
         public Explanation explain(LeafReaderContext context, int doc) {
           throw new UnsupportedOperationException();
         }
@@ -119,6 +113,11 @@ public class TestBooleanScorer extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public boolean equals(Object obj) {
       return this == obj;
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestConjunctionDISI.java b/lucene/core/src/test/org/apache/lucene/search/TestConjunctionDISI.java
index a9fd287..e729ed6 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestConjunctionDISI.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestConjunctionDISI.java
@@ -22,10 +22,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.util.BitDocIdSet;
 import org.apache.lucene.util.FixedBitSet;
 import org.apache.lucene.util.LuceneTestCase;
@@ -92,11 +90,6 @@ public class TestConjunctionDISI extends LuceneTestCase {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       return null;
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreQuery.java
index 3e15070..eee7bf9 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreQuery.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestConstantScoreQuery.java
@@ -19,15 +19,11 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.MultiReader;
 import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
@@ -158,6 +154,11 @@ public class TestConstantScoreQuery extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      in.visit(visitor);
+    }
+
+    @Override
     public boolean equals(Object other) {
       return sameClassAs(other) &&
              in.equals(((QueryWrapper) other).in);
@@ -230,17 +231,4 @@ public class TestConstantScoreQuery extends LuceneTestCase {
     dir.close();
   }
 
-  public void testExtractTerms() throws Exception {
-    final IndexSearcher searcher = newSearcher(new MultiReader());
-    final TermQuery termQuery = new TermQuery(new Term("foo", "bar"));
-    final Query csq = searcher.rewrite(new ConstantScoreQuery(termQuery));
-
-    final Set<Term> scoringTerms = new HashSet<>();
-    searcher.createWeight(csq, ScoreMode.COMPLETE, 1).extractTerms(scoringTerms);
-    assertEquals(Collections.emptySet(), scoringTerms);
-
-    final Set<Term> matchingTerms = new HashSet<>();
-    searcher.createWeight(csq, ScoreMode.COMPLETE_NO_SCORES, 1).extractTerms(matchingTerms);
-    assertEquals(Collections.singleton(new Term("foo", "bar")), matchingTerms);
-  }
 }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestLRUQueryCache.java b/lucene/core/src/test/org/apache/lucene/search/TestLRUQueryCache.java
index 2e3a4f1..6cc4fb9 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestLRUQueryCache.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestLRUQueryCache.java
@@ -29,7 +29,6 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -379,6 +378,11 @@ public class TestLRUQueryCache extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public boolean equals(Object other) {
       return sameClassAs(other) &&
              id == ((DummyQuery) other).id;
@@ -973,6 +977,11 @@ public class TestLRUQueryCache extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return "BadQuery";
     }
@@ -1305,11 +1314,6 @@ public class TestLRUQueryCache extends LuceneTestCase {
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
       return new Weight(this) {
         @Override
-        public void extractTerms(Set<Term> terms) {
-
-        }
-
-        @Override
         public Explanation explain(LeafReaderContext context, int doc) throws IOException {
           return null;
         }
@@ -1327,6 +1331,11 @@ public class TestLRUQueryCache extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return "NoCacheQuery";
     }
@@ -1411,6 +1420,11 @@ public class TestLRUQueryCache extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public boolean equals(Object other) {
       return sameClassAs(other);
     }
@@ -1494,6 +1508,11 @@ public class TestLRUQueryCache extends LuceneTestCase {
 
       };
     }
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
   }
 
   public void testDocValuesUpdatesDontBreakCache() throws IOException {
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java b/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
index 96a34a5..9da8d1d 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestMaxScoreSumPropagator.java
@@ -22,10 +22,8 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
 
@@ -40,11 +38,6 @@ public class TestMaxScoreSumPropagator extends LuceneTestCase {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       return null;
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestMultiTermQueryRewrites.java b/lucene/core/src/test/org/apache/lucene/search/TestMultiTermQueryRewrites.java
index 44f8d8b..e24e62b 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestMultiTermQueryRewrites.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestMultiTermQueryRewrites.java
@@ -183,6 +183,11 @@ public class TestMultiTermQueryRewrites extends LuceneTestCase {
       public String toString(String field) {
         return "dummy";
       }
+
+      @Override
+      public void visit(QueryVisitor visitor) {
+
+      }
     };
     mtq.setRewriteMethod(method);
     final Query q1 = searcher.rewrite(mtq);
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestNeedsScores.java b/lucene/core/src/test/org/apache/lucene/search/TestNeedsScores.java
index 03f78ef..3cdbb95 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestNeedsScores.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestNeedsScores.java
@@ -122,6 +122,11 @@ public class TestNeedsScores extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      in.visit(visitor);
+    }
+
+    @Override
     public int hashCode() {
       final int prime = 31;
       int result = classHash();
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestPrefixRandom.java b/lucene/core/src/test/org/apache/lucene/search/TestPrefixRandom.java
index f332a36..8180379 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestPrefixRandom.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestPrefixRandom.java
@@ -109,6 +109,11 @@ public class TestPrefixRandom extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public boolean equals(Object obj) {
       if (super.equals(obj) == false) {
         return false;
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestQueryRescorer.java b/lucene/core/src/test/org/apache/lucene/search/TestQueryRescorer.java
index 2e95653..f885f56 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestQueryRescorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestQueryRescorer.java
@@ -20,7 +20,6 @@ package org.apache.lucene.search;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Comparator;
-import java.util.Set;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
@@ -423,10 +422,6 @@ public class TestQueryRescorer extends LuceneTestCase {
       return new Weight(FixedScoreQuery.this) {
 
         @Override
-        public void extractTerms(Set<Term> terms) {
-        }
-
-        @Override
         public Scorer scorer(final LeafReaderContext context) throws IOException {
 
           return new Scorer(this) {
@@ -500,6 +495,11 @@ public class TestQueryRescorer extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return "FixedScoreQuery " + idToNum.length + " ids; reverse=" + reverse;
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestQueryVisitor.java b/lucene/core/src/test/org/apache/lucene/search/TestQueryVisitor.java
new file mode 100644
index 0000000..f1a4310
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestQueryVisitor.java
@@ -0,0 +1,333 @@
+/*
+ * 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.lucene.search;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.spans.SpanNearQuery;
+import org.apache.lucene.search.spans.SpanQuery;
+import org.apache.lucene.search.spans.SpanTermQuery;
+import org.apache.lucene.util.LuceneTestCase;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+public class TestQueryVisitor extends LuceneTestCase {
+
+  private static final Query query = new BooleanQuery.Builder()
+      .add(new TermQuery(new Term("field1", "t1")), BooleanClause.Occur.MUST)
+      .add(new BooleanQuery.Builder()
+          .add(new TermQuery(new Term("field1", "tm2")), BooleanClause.Occur.SHOULD)
+          .add(new BoostQuery(new TermQuery(new Term("field1", "tm3")), 2), BooleanClause.Occur.SHOULD)
+          .build(), BooleanClause.Occur.MUST)
+      .add(new BoostQuery(new PhraseQuery.Builder()
+          .add(new Term("field1", "term4"))
+          .add(new Term("field1", "term5"))
+          .build(), 3), BooleanClause.Occur.MUST)
+      .add(new SpanNearQuery(new SpanQuery[]{
+          new SpanTermQuery(new Term("field1", "term6")),
+          new SpanTermQuery(new Term("field1", "term7"))
+      }, 2, true), BooleanClause.Occur.MUST)
+      .add(new TermQuery(new Term("field1", "term8")), BooleanClause.Occur.MUST_NOT)
+      .add(new PrefixQuery(new Term("field1", "term9")), BooleanClause.Occur.SHOULD)
+      .add(new BoostQuery(new BooleanQuery.Builder()
+          .add(new BoostQuery(new TermQuery(new Term("field2", "term10")), 3), BooleanClause.Occur.MUST)
+          .build(), 2), BooleanClause.Occur.SHOULD)
+      .build();
+
+  public void testExtractTermsEquivalent() {
+    Set<Term> terms = new HashSet<>();
+    Set<Term> expected = new HashSet<>(Arrays.asList(
+        new Term("field1", "t1"), new Term("field1", "tm2"),
+        new Term("field1", "tm3"), new Term("field1", "term4"),
+        new Term("field1", "term5"), new Term("field1", "term6"),
+        new Term("field1", "term7"), new Term("field2", "term10")
+    ));
+    query.visit(QueryVisitor.termCollector(terms));
+    assertThat(terms, equalTo(expected));
+  }
+
+  public void extractAllTerms() {
+    Set<Term> terms = new HashSet<>();
+    QueryVisitor visitor = new QueryVisitor() {
+      @Override
+      public void consumeTerms(Query query, Term... ts) {
+        terms.addAll(Arrays.asList(ts));
+      }
+      @Override
+      public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
+        return this;
+      }
+    };
+    Set<Term> expected = new HashSet<>(Arrays.asList(
+        new Term("field1", "t1"), new Term("field1", "tm2"),
+        new Term("field1", "tm3"), new Term("field1", "term4"),
+        new Term("field1", "term5"), new Term("field1", "term6"),
+        new Term("field1", "term7"), new Term("field1", "term8"),
+        new Term("field2", "term10")
+    ));
+    query.visit(visitor);
+    assertThat(terms, equalTo(expected));
+  }
+
+  public void extractTermsFromField() {
+    final Set<Term> actual = new HashSet<>();
+    Set<Term> expected = new HashSet<>(Arrays.asList(new Term("field2", "term10")));
+    query.visit(new QueryVisitor(){
+      @Override
+      public boolean acceptField(String field) {
+        return "field2".equals(field);
+      }
+      @Override
+      public void consumeTerms(Query query, Term... terms) {
+        actual.addAll(Arrays.asList(terms));
+      }
+    });
+    assertThat(actual, equalTo(expected));
+  }
+
+  static class BoostedTermExtractor extends QueryVisitor {
+
+    final float boost;
+    final Map<Term, Float> termsToBoosts;
+
+    BoostedTermExtractor(float boost, Map<Term, Float> termsToBoosts) {
+      this.boost = boost;
+      this.termsToBoosts = termsToBoosts;
+    }
+
+    @Override
+    public void consumeTerms(Query query, Term... terms) {
+      for (Term term : terms) {
+        termsToBoosts.put(term, boost);
+      }
+    }
+
+    @Override
+    public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
+      if (parent instanceof BoostQuery) {
+        return new BoostedTermExtractor(boost * ((BoostQuery)parent).getBoost(), termsToBoosts);
+      }
+      return super.getSubVisitor(occur, parent);
+    }
+  }
+
+  public void testExtractTermsAndBoosts() {
+    Map<Term, Float> termsToBoosts = new HashMap<>();
+    query.visit(new BoostedTermExtractor(1, termsToBoosts));
+    Map<Term, Float> expected = new HashMap<>();
+    expected.put(new Term("field1", "t1"), 1f);
+    expected.put(new Term("field1", "tm2"), 1f);
+    expected.put(new Term("field1", "tm3"), 2f);
+    expected.put(new Term("field1", "term4"), 3f);
+    expected.put(new Term("field1", "term5"), 3f);
+    expected.put(new Term("field1", "term6"), 1f);
+    expected.put(new Term("field1", "term7"), 1f);
+    expected.put(new Term("field2", "term10"), 6f);
+    assertThat(termsToBoosts, equalTo(expected));
+  }
+
+  public void testLeafQueryTypeCounts() {
+    Map<Class<? extends Query>, Integer> queryCounts = new HashMap<>();
+    query.visit(new QueryVisitor() {
+
+      private void countQuery(Query q) {
+        queryCounts.compute(q.getClass(), (query, i) -> {
+          if (i == null) {
+            return 1;
+          }
+          return i + 1;
+        });
+      }
+
+      @Override
+      public void consumeTerms(Query query, Term... terms) {
+        countQuery(query);
+      }
+
+      @Override
+      public void visitLeaf(Query query) {
+        countQuery(query);
+      }
+
+    });
+    assertEquals(4, queryCounts.get(TermQuery.class).intValue());
+    assertEquals(1, queryCounts.get(PhraseQuery.class).intValue());
+  }
+
+  static abstract class QueryNode extends QueryVisitor {
+
+    final List<QueryNode> children = new ArrayList<>();
+
+    abstract int getWeight();
+    abstract void collectTerms(Set<Term> terms);
+    abstract boolean nextTermSet();
+
+    @Override
+    public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
+      if (occur == BooleanClause.Occur.MUST || occur == BooleanClause.Occur.FILTER) {
+        QueryNode n = new ConjunctionNode();
+        children.add(n);
+        return n;
+      }
+      if (occur == BooleanClause.Occur.MUST_NOT) {
+        return QueryVisitor.EMPTY_VISITOR;
+      }
+      if (parent instanceof BooleanQuery) {
+        BooleanQuery bq = (BooleanQuery) parent;
+        if (bq.getClauses(BooleanClause.Occur.MUST).size() > 0 || bq.getClauses(BooleanClause.Occur.FILTER).size() > 0) {
+          return QueryVisitor.EMPTY_VISITOR;
+        }
+      }
+      DisjunctionNode n = new DisjunctionNode();
+      children.add(n);
+      return n;
+    }
+  }
+
+  static class TermNode extends QueryNode {
+
+    final Term term;
+
+    TermNode(Term term) {
+      this.term = term;
+    }
+
+    @Override
+    int getWeight() {
+      return term.text().length();
+    }
+
+    @Override
+    void collectTerms(Set<Term> terms) {
+      terms.add(term);
+    }
+
+    @Override
+    boolean nextTermSet() {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return "TERM(" + term.toString() + ")";
+    }
+  }
+
+  static class ConjunctionNode extends QueryNode {
+
+    @Override
+    int getWeight() {
+      children.sort(Comparator.comparingInt(QueryNode::getWeight));
+      return children.get(0).getWeight();
+    }
+
+    @Override
+    void collectTerms(Set<Term> terms) {
+      children.sort(Comparator.comparingInt(QueryNode::getWeight));
+      children.get(0).collectTerms(terms);
+    }
+
+    @Override
+    boolean nextTermSet() {
+      children.sort(Comparator.comparingInt(QueryNode::getWeight));
+      if (children.get(0).nextTermSet()) {
+        return true;
+      }
+      if (children.size() == 1) {
+        return false;
+      }
+      children.remove(0);
+      return true;
+    }
+
+    @Override
+    public void consumeTerms(Query query, Term... terms) {
+      for (Term term : terms) {
+        children.add(new TermNode(term));
+      }
+    }
+
+    @Override
+    public String toString() {
+      return children.stream().map(QueryNode::toString).collect(Collectors.joining(",", "AND(", ")"));
+    }
+  }
+
+  static class DisjunctionNode extends QueryNode {
+
+    @Override
+    int getWeight() {
+      children.sort(Comparator.comparingInt(QueryNode::getWeight).reversed());
+      return children.get(0).getWeight();
+    }
+
+    @Override
+    void collectTerms(Set<Term> terms) {
+      for (QueryNode child : children) {
+        child.collectTerms(terms);
+      }
+    }
+
+    @Override
+    boolean nextTermSet() {
+      boolean next = false;
+      for (QueryNode child : children) {
+        next |= child.nextTermSet();
+      }
+      return next;
+    }
+
+    @Override
+    public void consumeTerms(Query query, Term... terms) {
+      for (Term term : terms) {
+        children.add(new TermNode(term));
+      }
+    }
+
+    @Override
+    public String toString() {
+      return children.stream().map(QueryNode::toString).collect(Collectors.joining(",", "OR(", ")"));
+    }
+  }
+
+  public void testExtractMatchingTermSet() {
+    QueryNode extractor = new ConjunctionNode();
+    query.visit(extractor);
+    Set<Term> minimumTermSet = new HashSet<>();
+    extractor.collectTerms(minimumTermSet);
+
+    Set<Term> expected1 = new HashSet<>(Collections.singletonList(new Term("field1", "t1")));
+    assertThat(minimumTermSet, equalTo(expected1));
+    assertTrue(extractor.nextTermSet());
+    Set<Term> expected2 = new HashSet<>(Arrays.asList(new Term("field1", "tm2"), new Term("field1", "tm3")));
+    minimumTermSet.clear();
+    extractor.collectTerms(minimumTermSet);
+    assertThat(minimumTermSet, equalTo(expected2));
+  }
+
+}
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestRegexpRandom2.java b/lucene/core/src/test/org/apache/lucene/search/TestRegexpRandom2.java
index 78a4352..9c2810c 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestRegexpRandom2.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestRegexpRandom2.java
@@ -40,9 +40,9 @@ import org.apache.lucene.util.CharsRefBuilder;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
 import org.apache.lucene.util.UnicodeUtil;
+import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.AutomatonTestUtil;
 import org.apache.lucene.util.automaton.CharacterRunAutomaton;
-import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.RegExp;
 
 /**
@@ -139,6 +139,11 @@ public class TestRegexpRandom2 extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public boolean equals(Object obj) {
       if (super.equals(obj) == false) {
         return false;
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestScorerPerf.java b/lucene/core/src/test/org/apache/lucene/search/TestScorerPerf.java
index be3f969..20e7857 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestScorerPerf.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestScorerPerf.java
@@ -142,7 +142,12 @@ public class TestScorerPerf extends LuceneTestCase {
         }
       };
     }
-    
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
     @Override
     public String toString(String field) {
       return "randomBitSetFilter";
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestSortRandom.java b/lucene/core/src/test/org/apache/lucene/search/TestSortRandom.java
index 136026e..9de3370 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestSortRandom.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestSortRandom.java
@@ -258,6 +258,11 @@ public class TestSortRandom extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return "RandomFilter(density=" + density + ")";
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestUsageTrackingFilterCachingPolicy.java b/lucene/core/src/test/org/apache/lucene/search/TestUsageTrackingFilterCachingPolicy.java
index c817623..74112e8 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestUsageTrackingFilterCachingPolicy.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestUsageTrackingFilterCachingPolicy.java
@@ -132,6 +132,11 @@ public class TestUsageTrackingFilterCachingPolicy extends LuceneTestCase {
       };
     }
 
+    @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
   }
 
 }
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
index 7d1b2cb..2a48cb3 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
@@ -355,6 +355,11 @@ public class TestWANDScorer extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
       return new FilterWeight(query.createWeight(searcher, scoreMode, boost)) {
         @Override
diff --git a/lucene/core/src/test/org/apache/lucene/search/spans/JustCompileSearchSpans.java b/lucene/core/src/test/org/apache/lucene/search/spans/JustCompileSearchSpans.java
index 3244c1d..42e4c3c 100644
--- a/lucene/core/src/test/org/apache/lucene/search/spans/JustCompileSearchSpans.java
+++ b/lucene/core/src/test/org/apache/lucene/search/spans/JustCompileSearchSpans.java
@@ -20,6 +20,7 @@ package org.apache.lucene.search.spans;
 import java.io.IOException;
 
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 
 /**
@@ -99,6 +100,11 @@ final class JustCompileSearchSpans {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      throw new UnsupportedOperationException(UNSUPPORTED_MSG);
+    }
+
+    @Override
     public String toString(String field) {
       throw new UnsupportedOperationException(UNSUPPORTED_MSG);
     }
diff --git a/lucene/core/src/test/org/apache/lucene/search/spans/TestFieldMaskingSpanQuery.java b/lucene/core/src/test/org/apache/lucene/search/spans/TestFieldMaskingSpanQuery.java
index f72ea66..4005ae5 100644
--- a/lucene/core/src/test/org/apache/lucene/search/spans/TestFieldMaskingSpanQuery.java
+++ b/lucene/core/src/test/org/apache/lucene/search/spans/TestFieldMaskingSpanQuery.java
@@ -17,6 +17,9 @@
 package org.apache.lucene.search.spans;
 
 
+import java.util.HashSet;
+import java.util.Set;
+
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
@@ -27,6 +30,7 @@ import org.apache.lucene.search.CheckHits;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.QueryUtils;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.similarities.TFIDFSimilarity;
 import org.apache.lucene.store.Directory;
@@ -34,9 +38,6 @@ import org.apache.lucene.util.LuceneTestCase;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
-import java.util.HashSet;
-import java.util.Set;
-
 import static org.apache.lucene.search.spans.SpanTestUtil.assertFinished;
 import static org.apache.lucene.search.spans.SpanTestUtil.assertNext;
 
@@ -143,7 +144,7 @@ public class TestFieldMaskingSpanQuery extends LuceneTestCase {
     QueryUtils.checkEqual(q, qr);
 
     Set<Term> terms = new HashSet<>();
-    qr.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1f).extractTerms(terms);
+    qr.visit(QueryVisitor.termCollector(terms));
     assertEquals(1, terms.size());
   }
   
@@ -163,7 +164,7 @@ public class TestFieldMaskingSpanQuery extends LuceneTestCase {
     QueryUtils.checkUnequal(q, qr);
 
     Set<Term> terms = new HashSet<>();
-    qr.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1f).extractTerms(terms);
+    qr.visit(QueryVisitor.termCollector(terms));
     assertEquals(2, terms.size());
   }
   
@@ -177,7 +178,7 @@ public class TestFieldMaskingSpanQuery extends LuceneTestCase {
     QueryUtils.checkEqual(q, qr);
 
     HashSet<Term> set = new HashSet<>();
-    qr.createWeight(searcher, ScoreMode.COMPLETE, 1f).extractTerms(set);
+    qr.visit(QueryVisitor.termCollector(set));
     assertEquals(2, set.size());
   }
   
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/DrillDownQuery.java b/lucene/facet/src/java/org/apache/lucene/facet/DrillDownQuery.java
index 1ea2613..badfe81 100644
--- a/lucene/facet/src/java/org/apache/lucene/facet/DrillDownQuery.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/DrillDownQuery.java
@@ -30,6 +30,7 @@ import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TermQuery;
 
 /**
@@ -149,6 +150,11 @@ public final class DrillDownQuery extends Query {
     return getBooleanQuery().toString(field);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
   private BooleanQuery getBooleanQuery() {
     BooleanQuery.Builder bq = new BooleanQuery.Builder();
     if (baseQuery != null) {
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/DrillSidewaysQuery.java b/lucene/facet/src/java/org/apache/lucene/facet/DrillSidewaysQuery.java
index 82c642a..748ed82 100644
--- a/lucene/facet/src/java/org/apache/lucene/facet/DrillSidewaysQuery.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/DrillSidewaysQuery.java
@@ -20,12 +20,10 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.facet.DrillSidewaysScorer.DocsAndCost;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BulkScorer;
 import org.apache.lucene.search.Collector;
 import org.apache.lucene.search.ConstantScoreScorer;
@@ -33,6 +31,7 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -78,6 +77,11 @@ class DrillSidewaysQuery extends Query {
       return new DrillSidewaysQuery(newQuery, drillDownCollector, drillSidewaysCollectors, drillDownQueries, scoreSubDocsAtOnce);
     }
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
   
   @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
@@ -89,9 +93,6 @@ class DrillSidewaysQuery extends Query {
 
     return new Weight(DrillSidewaysQuery.this) {
       @Override
-      public void extractTerms(Set<Term> terms) {}
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         return baseWeight.explain(context, doc);
       }
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java b/lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java
index afc4c4f..ad0ba67 100644
--- a/lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java
@@ -28,6 +28,7 @@ import org.apache.lucene.search.DoubleValues;
 import org.apache.lucene.search.DoubleValuesSource;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -142,6 +143,11 @@ public final class DoubleRange extends Range {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public Query rewrite(IndexReader reader) throws IOException {
       if (fastMatchQuery != null) {
         final Query fastMatchRewritten = fastMatchQuery.rewrite(reader);
diff --git a/lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java b/lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java
index eed293f..f69ba76 100644
--- a/lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java
@@ -28,6 +28,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LongValues;
 import org.apache.lucene.search.LongValuesSource;
 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;
@@ -134,6 +135,11 @@ public final class LongRange extends Range {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public Query rewrite(IndexReader reader) throws IOException {
       if (fastMatchQuery != null) {
         final Query fastMatchRewritten = fastMatchQuery.rewrite(reader);
diff --git a/lucene/facet/src/test/org/apache/lucene/facet/TestDrillSideways.java b/lucene/facet/src/test/org/apache/lucene/facet/TestDrillSideways.java
index 288f6cc..6dc790d 100644
--- a/lucene/facet/src/test/org/apache/lucene/facet/TestDrillSideways.java
+++ b/lucene/facet/src/test/org/apache/lucene/facet/TestDrillSideways.java
@@ -52,6 +52,7 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Scorer;
@@ -750,6 +751,11 @@ public class TestDrillSideways extends FacetTestCase {
           }
 
           @Override
+          public void visit(QueryVisitor visitor) {
+
+          }
+
+          @Override
           public String toString(String field) {
             return "drillSidewaysTestFilter";
           }
diff --git a/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java b/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java
index 2408286..441675c 100644
--- a/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java
+++ b/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java
@@ -53,6 +53,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LongValuesSource;
 import org.apache.lucene.search.MatchAllDocsQuery;
 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.Weight;
@@ -715,6 +716,11 @@ public class TestRangeFacetCounts extends FacetTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      in.visit(visitor);
+    }
+
+    @Override
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
       final Weight in = this.in.createWeight(searcher, scoreMode, boost);
       return new FilterWeight(in) {
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/highlight/QueryTermExtractor.java b/lucene/highlighter/src/java/org/apache/lucene/search/highlight/QueryTermExtractor.java
index 9d3784a..0b5738f 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/highlight/QueryTermExtractor.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/highlight/QueryTermExtractor.java
@@ -15,19 +15,20 @@
  * limitations under the License.
  */
 package org.apache.lucene.search.highlight;
+
 import java.io.IOException;
 import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Predicate;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.MultiReader;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.QueryVisitor;
 
 /**
  * Utility class used to extract the terms used in a query, plus any weights.
@@ -100,10 +101,10 @@ public final class QueryTermExtractor
    * @param fieldName  The fieldName used to filter query terms
    * @return an array of the terms used in a query, plus their weights.
    */
-  public static final WeightedTerm[] getTerms(Query query, boolean prohibited, String fieldName)
-  {
-    HashSet<WeightedTerm> terms=new HashSet<>();
-    getTerms(query, 1f, terms,prohibited,fieldName);
+  public static WeightedTerm[] getTerms(Query query, boolean prohibited, String fieldName) {
+    HashSet<WeightedTerm> terms = new HashSet<>();
+    Predicate<String> fieldSelector = fieldName == null ? f -> true : fieldName::equals;
+    query.visit(new BoostedTermExtractor(1, terms, prohibited, fieldSelector));
     return terms.toArray(new WeightedTerm[0]);
   }
 
@@ -119,50 +120,45 @@ public final class QueryTermExtractor
       return getTerms(query,prohibited,null);
   }
 
-  private static final void getTerms(Query query, float boost, HashSet<WeightedTerm> terms, boolean prohibited, String fieldName) {
-    try {
-      if (query instanceof BoostQuery) {
-        BoostQuery boostQuery = (BoostQuery) query;
-        getTerms(boostQuery.getQuery(), boost * boostQuery.getBoost(), terms, prohibited, fieldName);
-      } else if (query instanceof BooleanQuery)
-        getTermsFromBooleanQuery((BooleanQuery) query, boost, terms, prohibited, fieldName);
-      else {
-        HashSet<Term> nonWeightedTerms = new HashSet<>();
-        try {
-          EMPTY_INDEXSEARCHER.createWeight(EMPTY_INDEXSEARCHER.rewrite(query), ScoreMode.COMPLETE_NO_SCORES, 1).extractTerms(nonWeightedTerms);
-        } catch (IOException bogus) {
-          throw new RuntimeException("Should not happen on an empty index", bogus);
-        }
-        for (Iterator<Term> iter = nonWeightedTerms.iterator(); iter.hasNext(); ) {
-          Term term = iter.next();
-          if ((fieldName == null) || (term.field().equals(fieldName))) {
-            terms.add(new WeightedTerm(boost, term.text()));
-          }
-        }
+  private static class BoostedTermExtractor extends QueryVisitor {
+
+    final float boost;
+    final Set<WeightedTerm> terms;
+    final boolean includeProhibited;
+    final Predicate<String> fieldSelector;
+
+    private BoostedTermExtractor(float boost, Set<WeightedTerm> terms, boolean includeProhibited,
+                                 Predicate<String> fieldSelector) {
+      this.boost = boost;
+      this.terms = terms;
+      this.includeProhibited = includeProhibited;
+      this.fieldSelector = fieldSelector;
+    }
+
+    @Override
+    public boolean acceptField(String field) {
+      return fieldSelector.test(field);
+    }
+
+    @Override
+    public void consumeTerms(Query query, Term... terms) {
+      for (Term term : terms) {
+        this.terms.add(new WeightedTerm(boost, term.text()));
       }
-    } catch (UnsupportedOperationException ignore) {
-      //this is non-fatal for our purposes
     }
-  }
 
-  /**
-   * extractTerms is currently the only query-independent means of introspecting queries but it only reveals
-   * a list of terms for that query - not the boosts each individual term in that query may or may not have.
-   * "Container" queries such as BooleanQuery should be unwrapped to get at the boost info held
-   * in each child element.
-   * Some discussion around this topic here:
-   * http://www.gossamer-threads.com/lists/lucene/java-dev/34208?search_string=introspection;#34208
-   * Unfortunately there seemed to be limited interest in requiring all Query objects to implement
-   * something common which would allow access to child queries so what follows here are query-specific
-   * implementations for accessing embedded query elements.
-   */
-  private static final void getTermsFromBooleanQuery(BooleanQuery query, float boost, HashSet<WeightedTerm> terms, boolean prohibited, String fieldName)
-  {
-    for (BooleanClause clause : query)
-    {
-      if (prohibited || clause.getOccur()!=BooleanClause.Occur.MUST_NOT)
-        getTerms(clause.getQuery(), boost, terms, prohibited, fieldName);
+    @Override
+    public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
+      if (parent instanceof BoostQuery) {
+        float newboost = boost * ((BoostQuery)parent).getBoost();
+        return new BoostedTermExtractor(newboost, terms, includeProhibited, fieldSelector);
+      }
+      if (occur == BooleanClause.Occur.MUST_NOT && includeProhibited == false) {
+        return QueryVisitor.EMPTY_VISITOR;
+      }
+      return this;
     }
+
   }
 
 }
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/highlight/WeightedSpanTermExtractor.java b/lucene/highlighter/src/java/org/apache/lucene/search/highlight/WeightedSpanTermExtractor.java
index bc02621..ee0a187 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/highlight/WeightedSpanTermExtractor.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/highlight/WeightedSpanTermExtractor.java
@@ -51,6 +51,7 @@ import org.apache.lucene.search.MultiPhraseQuery;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.PhraseQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.SynonymQuery;
 import org.apache.lucene.search.TermQuery;
@@ -308,10 +309,10 @@ public class WeightedSpanTermExtractor {
       for (final String field : fieldNames) {
         final SpanQuery rewrittenQuery = (SpanQuery) spanQuery.rewrite(getLeafContext().reader());
         queries.put(field, rewrittenQuery);
-        rewrittenQuery.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, boost).extractTerms(nonWeightedTerms);
+        rewrittenQuery.visit(QueryVisitor.termCollector(nonWeightedTerms));
       }
     } else {
-      spanQuery.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, boost).extractTerms(nonWeightedTerms);
+      spanQuery.visit(QueryVisitor.termCollector(nonWeightedTerms));
     }
 
     List<PositionSpan> spanPositions = new ArrayList<>();
@@ -378,7 +379,7 @@ public class WeightedSpanTermExtractor {
   protected void extractWeightedTerms(Map<String,WeightedSpanTerm> terms, Query query, float boost) throws IOException {
     Set<Term> nonWeightedTerms = new HashSet<>();
     final IndexSearcher searcher = new IndexSearcher(getLeafContext());
-    searcher.createWeight(searcher.rewrite(query), ScoreMode.COMPLETE_NO_SCORES, 1).extractTerms(nonWeightedTerms);
+    searcher.rewrite(query).visit(QueryVisitor.termCollector(nonWeightedTerms));
 
     for (final Term queryTerm : nonWeightedTerms) {
 
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
index 30dbdd4..1f4455f 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
@@ -19,10 +19,8 @@ package org.apache.lucene.search.uhighlight;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.FilteringTokenFilter;
@@ -30,7 +28,6 @@ import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.memory.MemoryIndex;
-import org.apache.lucene.search.Query;
 import org.apache.lucene.search.spans.SpanQuery;
 import org.apache.lucene.util.automaton.Automata;
 import org.apache.lucene.util.automaton.CharacterRunAutomaton;
@@ -47,21 +44,19 @@ public class MemoryIndexOffsetStrategy extends AnalysisOffsetStrategy {
   private final LeafReader memIndexLeafReader;
   private final CharacterRunAutomaton preMemIndexFilterAutomaton;
 
-  public MemoryIndexOffsetStrategy(UHComponents components, Analyzer analyzer,
-                                   Function<Query, Collection<Query>> multiTermQueryRewrite) {
+  public MemoryIndexOffsetStrategy(UHComponents components, Analyzer analyzer) {
     super(components, analyzer);
     boolean storePayloads = components.getPhraseHelper().hasPositionSensitivity(); // might be needed
     memoryIndex = new MemoryIndex(true, storePayloads);//true==store offsets
     memIndexLeafReader = (LeafReader) memoryIndex.createSearcher().getIndexReader(); // appears to be re-usable
     // preFilter for MemoryIndex
-    preMemIndexFilterAutomaton = buildCombinedAutomaton(components, multiTermQueryRewrite);
+    preMemIndexFilterAutomaton = buildCombinedAutomaton(components);
   }
 
   /**
    * Build one {@link CharacterRunAutomaton} matching any term the query might match.
    */
-  private static CharacterRunAutomaton buildCombinedAutomaton(UHComponents components,
-                                                              Function<Query, Collection<Query>> multiTermQueryRewrite) {
+  private static CharacterRunAutomaton buildCombinedAutomaton(UHComponents components) {
     List<CharacterRunAutomaton> allAutomata = new ArrayList<>();
     if (components.getTerms().length > 0) {
       allAutomata.add(new CharacterRunAutomaton(Automata.makeStringUnion(Arrays.asList(components.getTerms()))));
@@ -69,7 +64,7 @@ public class MemoryIndexOffsetStrategy extends AnalysisOffsetStrategy {
     Collections.addAll(allAutomata, components.getAutomata());
     for (SpanQuery spanQuery : components.getPhraseHelper().getSpanQueries()) {
       Collections.addAll(allAutomata,
-          MultiTermHighlighting.extractAutomata(spanQuery, components.getFieldMatcher(), true, multiTermQueryRewrite));//true==lookInSpan
+          MultiTermHighlighting.extractAutomata(spanQuery, components.getFieldMatcher(), true));//true==lookInSpan
     }
 
     if (allAutomata.size() == 1) {
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
index 57d5afa..d079599 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
@@ -17,33 +17,20 @@
 package org.apache.lucene.search.uhighlight;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
-import java.util.function.Function;
 import java.util.function.Predicate;
 
-import org.apache.lucene.queries.function.FunctionScoreQuery;
 import org.apache.lucene.search.AutomatonQuery;
 import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.BoostQuery;
-import org.apache.lucene.search.ConstantScoreQuery;
-import org.apache.lucene.search.DisjunctionMaxQuery;
 import org.apache.lucene.search.FuzzyQuery;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.spans.SpanBoostQuery;
-import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper;
-import org.apache.lucene.search.spans.SpanNearQuery;
-import org.apache.lucene.search.spans.SpanNotQuery;
-import org.apache.lucene.search.spans.SpanOrQuery;
-import org.apache.lucene.search.spans.SpanPositionCheckQuery;
+import org.apache.lucene.search.QueryVisitor;
+import org.apache.lucene.search.spans.SpanQuery;
 import org.apache.lucene.util.UnicodeUtil;
 import org.apache.lucene.util.automaton.Automata;
 import org.apache.lucene.util.automaton.Automaton;
 import org.apache.lucene.util.automaton.ByteRunAutomaton;
 import org.apache.lucene.util.automaton.CharacterRunAutomaton;
-import org.apache.lucene.util.automaton.LevenshteinAutomata;
 import org.apache.lucene.util.automaton.Operations;
 
 /**
@@ -51,7 +38,7 @@ import org.apache.lucene.util.automaton.Operations;
  *
  * @lucene.internal
  */
-class MultiTermHighlighting {
+final class MultiTermHighlighting {
   private MultiTermHighlighting() {
   }
 
@@ -59,138 +46,117 @@ class MultiTermHighlighting {
    * Extracts MultiTermQueries that match the provided field predicate.
    * Returns equivalent automata that will match terms.
    */
-  public static CharacterRunAutomaton[] extractAutomata(Query query,
-                                                        Predicate<String> fieldMatcher,
-                                                        boolean lookInSpan,
-                                                        Function<Query, Collection<Query>> preRewriteFunc) {
-    // TODO Lucene needs a Query visitor API!  LUCENE-3041
-
-    List<CharacterRunAutomaton> list = new ArrayList<>();
-    Collection<Query> customSubQueries = preRewriteFunc.apply(query);
-    if (customSubQueries != null) {
-      for (Query sub : customSubQueries) {
-        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
-      }
-    } else if (query instanceof BooleanQuery) {
-      for (BooleanClause clause : (BooleanQuery) query) {
-        if (!clause.isProhibited()) {
-          list.addAll(Arrays.asList(extractAutomata(clause.getQuery(), fieldMatcher, lookInSpan, preRewriteFunc)));
-        }
-      }
-    } else if (query instanceof ConstantScoreQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((ConstantScoreQuery) query).getQuery(), fieldMatcher, lookInSpan,
-          preRewriteFunc)));
-    } else if (query instanceof BoostQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((BoostQuery) query).getQuery(), fieldMatcher, lookInSpan,
-          preRewriteFunc)));
-    } else if (query instanceof FunctionScoreQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((FunctionScoreQuery) query).getWrappedQuery(), fieldMatcher,
-          lookInSpan, preRewriteFunc)));
-    } else if (query instanceof DisjunctionMaxQuery) {
-      for (Query sub : ((DisjunctionMaxQuery) query).getDisjuncts()) {
-        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
-      }
-    } else if (lookInSpan && query instanceof SpanOrQuery) {
-      for (Query sub : ((SpanOrQuery) query).getClauses()) {
-        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
-      }
-    } else if (lookInSpan && query instanceof SpanNearQuery) {
-      for (Query sub : ((SpanNearQuery) query).getClauses()) {
-        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
-      }
-    } else if (lookInSpan && query instanceof SpanNotQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanNotQuery) query).getInclude(), fieldMatcher, lookInSpan,
-          preRewriteFunc)));
-    } else if (lookInSpan && query instanceof SpanPositionCheckQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanPositionCheckQuery) query).getMatch(), fieldMatcher, lookInSpan,
-          preRewriteFunc)));
-    } else if (lookInSpan && query instanceof SpanBoostQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanBoostQuery) query).getQuery(), fieldMatcher, lookInSpan,
-          preRewriteFunc)));
-    } else if (lookInSpan && query instanceof SpanMultiTermQueryWrapper) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanMultiTermQueryWrapper<?>) query).getWrappedQuery(),
-          fieldMatcher, lookInSpan, preRewriteFunc)));
-    } else if (query instanceof FuzzyQuery) {
-      final FuzzyQuery fq = (FuzzyQuery) query;
-      if (fieldMatcher.test(fq.getField())) {
-        String utf16 = fq.getTerm().text();
-        int termText[] = new int[utf16.codePointCount(0, utf16.length())];
-        for (int cp, i = 0, j = 0; i < utf16.length(); i += Character.charCount(cp)) {
-          termText[j++] = cp = utf16.codePointAt(i);
-        }
-        int termLength = termText.length;
-        int prefixLength = Math.min(fq.getPrefixLength(), termLength);
-        String suffix = UnicodeUtil.newString(termText, prefixLength, termText.length - prefixLength);
-        LevenshteinAutomata builder = new LevenshteinAutomata(suffix, fq.getTranspositions());
-        String prefix = UnicodeUtil.newString(termText, 0, prefixLength);
-        Automaton automaton = builder.toAutomaton(fq.getMaxEdits(), prefix);
-        list.add(new CharacterRunAutomaton(automaton) {
-          @Override
-          public String toString() {
-            return fq.toString();
-          }
-        });
+  static CharacterRunAutomaton[] extractAutomata(Query query, Predicate<String> fieldMatcher, boolean lookInSpan) {
+
+    AutomataCollector collector = new AutomataCollector(lookInSpan, fieldMatcher);
+    query.visit(collector);
+    return collector.runAutomata.toArray(new CharacterRunAutomaton[0]);
+  }
+
+  private static class AutomataCollector extends QueryVisitor {
+
+    List<CharacterRunAutomaton> runAutomata = new ArrayList<>();
+    final boolean lookInSpan;
+    final Predicate<String> fieldMatcher;
+
+    private AutomataCollector(boolean lookInSpan, Predicate<String> fieldMatcher) {
+      this.lookInSpan = lookInSpan;
+      this.fieldMatcher = fieldMatcher;
+    }
+
+    @Override
+    public boolean acceptField(String field) {
+      return fieldMatcher.test(field);
+    }
+
+    @Override
+    public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
+      if (lookInSpan == false && parent instanceof SpanQuery) {
+        return QueryVisitor.EMPTY_VISITOR;
       }
-    } else if (query instanceof AutomatonQuery) {
-      final AutomatonQuery aq = (AutomatonQuery) query;
-      if (fieldMatcher.test(aq.getField())) {
+      return super.getSubVisitor(occur, parent);
+    }
 
-        if (aq.isAutomatonBinary() == false) { // note: is the case for WildcardQuery, RegexpQuery
-          list.add(new CharacterRunAutomaton(aq.getAutomaton()) {
+    @Override
+    public void visitLeaf(Query query) {
+      if (query instanceof AutomatonQuery) {
+        AutomatonQuery aq = (AutomatonQuery) query;
+        if (aq.isAutomatonBinary() == false) {
+          // WildcardQuery, RegexpQuery
+          runAutomata.add(new CharacterRunAutomaton(aq.getAutomaton()) {
             @Override
             public String toString() {
-              return aq.toString();
+              return query.toString();
             }
           });
-        } else { // note: is the case for PrefixQuery, TermRangeQuery
-          // byte oriented automaton:
-          list.add(new CharacterRunAutomaton(Automata.makeEmpty()) { // empty here is bogus just to satisfy API
-            //   TODO can we get access to the aq.compiledAutomaton.runAutomaton ?
-            ByteRunAutomaton byteRunAutomaton =
-                new ByteRunAutomaton(aq.getAutomaton(), true, Operations.DEFAULT_MAX_DETERMINIZED_STATES);
-
-            @Override
-            public boolean run(char[] chars, int offset, int length) {
-              int state = 0;
-              final int maxIdx = offset + length;
-              for (int i = offset; i < maxIdx; i++) {
-                final int code = chars[i];
-                int b;
-                // UTF16 to UTF8   (inlined logic from UnicodeUtil.UTF16toUTF8 )
-                if (code < 0x80) {
-                  state = byteRunAutomaton.step(state, code);
-                  if (state == -1) return false;
-                } else if (code < 0x800) {
-                  b = (0xC0 | (code >> 6));
-                  state = byteRunAutomaton.step(state, b);
-                  if (state == -1) return false;
-                  b = (0x80 | (code & 0x3F));
-                  state = byteRunAutomaton.step(state, b);
-                  if (state == -1) return false;
-                } else {
-                  // more complex
-                  byte[] utf8Bytes = new byte[4 * (maxIdx - i)];
-                  int utf8Len = UnicodeUtil.UTF16toUTF8(chars, i, maxIdx - i, utf8Bytes);
-                  for (int utfIdx = 0; utfIdx < utf8Len; utfIdx++) {
-                    state = byteRunAutomaton.step(state, utf8Bytes[utfIdx] & 0xFF);
-                    if (state == -1) return false;
-                  }
-                  break;
-                }
-              }
-              return byteRunAutomaton.isAccept(state);
-            }
-
+        }
+        else {
+          runAutomata.add(binaryToCharRunAutomaton(aq.getAutomaton(), query.toString()));
+        }
+      }
+      else if (query instanceof FuzzyQuery) {
+        FuzzyQuery fq = (FuzzyQuery) query;
+        if (fq.getMaxEdits() == 0 || fq.getPrefixLength() >= fq.getTerm().text().length()) {
+          consumeTerms(query, fq.getTerm());
+        }
+        else {
+          runAutomata.add(new CharacterRunAutomaton(fq.toAutomaton()){
             @Override
             public String toString() {
-              return aq.toString();
+              return query.toString();
             }
           });
         }
-
       }
     }
-    return list.toArray(new CharacterRunAutomaton[list.size()]);
+
   }
 
+  private static CharacterRunAutomaton binaryToCharRunAutomaton(Automaton binaryAutomaton, String description) {
+    return new CharacterRunAutomaton(Automata.makeEmpty()) { // empty here is bogus just to satisfy API
+      //   TODO can we get access to the aq.compiledAutomaton.runAutomaton ?
+      ByteRunAutomaton byteRunAutomaton =
+          new ByteRunAutomaton(binaryAutomaton, true, Operations.DEFAULT_MAX_DETERMINIZED_STATES);
+
+      @Override
+      public String toString() {
+        return description;
+      }
+
+      @Override
+      public boolean run(char[] chars, int offset, int length) {
+        int state = 0;
+        final int maxIdx = offset + length;
+        for (int i = offset; i < maxIdx; i++) {
+          final int code = chars[i];
+          int b;
+          // UTF16 to UTF8   (inlined logic from UnicodeUtil.UTF16toUTF8 )
+          if (code < 0x80) {
+            state = byteRunAutomaton.step(state, code);
+            if (state == -1) return false;
+          } else if (code < 0x800) {
+            b = (0xC0 | (code >> 6));
+            state = byteRunAutomaton.step(state, b);
+            if (state == -1) return false;
+            b = (0x80 | (code & 0x3F));
+            state = byteRunAutomaton.step(state, b);
+            if (state == -1) return false;
+          } else {
+            // more complex
+            byte[] utf8Bytes = new byte[4 * (maxIdx - i)];
+            int utf8Len = UnicodeUtil.UTF16toUTF8(chars, i, maxIdx - i, utf8Bytes);
+            for (int utfIdx = 0; utfIdx < utf8Len; utfIdx++) {
+              state = byteRunAutomaton.step(state, utf8Bytes[utfIdx] & 0xFF);
+              if (state == -1) return false;
+            }
+            break;
+          }
+        }
+        return byteRunAutomaton.isAccept(state);
+      }
+    };
+  }
+
+
+
 }
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/OffsetsEnum.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/OffsetsEnum.java
index 0e6a221..d399883 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/OffsetsEnum.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/OffsetsEnum.java
@@ -23,14 +23,13 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
 import java.util.PriorityQueue;
-import java.util.TreeSet;
 import java.util.function.Supplier;
 
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.BytesRefBuilder;
 import org.apache.lucene.util.IOUtils;
@@ -245,30 +244,24 @@ public abstract class OffsetsEnum implements Comparable<OffsetsEnum>, Closeable
      * See {@link Passage#getMatchTerms()}. */
     private BytesRef queryToTerm(Query query) {
       // compute an approximate BytesRef term of a Query.  We cache this since we're likely to see the same query again.
-      // Our approach is to call extractTerms and visit each term in order, concatenating them with an adjoining space.
+      // Our approach is to visit each matching term in order, concatenating them with an adjoining space.
       //  If we don't have any (perhaps due to an MTQ like a wildcard) then we fall back on the toString() of the query.
       return queryToTermMap.computeIfAbsent(query, (Query q) -> {
-        try {
-          BytesRefBuilder bytesRefBuilder = new BytesRefBuilder();
-          UnifiedHighlighter.EMPTY_INDEXSEARCHER
-              .createWeight(UnifiedHighlighter.EMPTY_INDEXSEARCHER.rewrite(q), ScoreMode.COMPLETE_NO_SCORES, 1f)
-              .extractTerms(new TreeSet<Term>() {
-            @Override
-            public boolean add(Term term) {
+        BytesRefBuilder bytesRefBuilder = new BytesRefBuilder();
+        q.visit(new QueryVisitor() {
+          @Override
+          public void consumeTerms(Query query, Term... terms) {
+            for (Term term : terms) {
               if (bytesRefBuilder.length() > 0) {
                 bytesRefBuilder.append((byte) ' ');
               }
               bytesRefBuilder.append(term.bytes());
-              return true;
             }
-          });
-          if (bytesRefBuilder.length() > 0) {
-            return bytesRefBuilder.get();
           }
-        } catch (IOException e) {//ignore
-          // go to fallback...
+        });
+        if (bytesRefBuilder.length() > 0) {
+          return bytesRefBuilder.get();
         }
-
         // fallback:  (likely a MultiTermQuery)
         return new BytesRef(q.toString());
       });
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
index ab439d0..47b770d 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
@@ -24,7 +24,6 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -40,6 +39,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.MultiTermQuery;
 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;
@@ -97,20 +97,6 @@ public class PhraseHelper {
 
     boolean[] mustRewriteHolder = {false}; // boolean wrapped in 1-ary array so it's mutable from inner class
 
-    // When we call Weight.extractTerms, we do it on clauses that are NOT position sensitive.
-    // We only want the to track a Set of bytes for the Term, not Term class with field part.
-    Set<Term> extractPosInsensitiveTermsTarget = new TreeSet<Term>() {
-      @Override
-      public boolean add(Term term) {
-        // don't call super.add; we don't actually use the superclass
-        if (fieldMatcher.test(term.field())) {
-          return positionInsensitiveTerms.add(term.bytes());
-        } else {
-          return false;
-        }
-      }
-    };
-
     // For TermQueries or other position insensitive queries, collect the Terms.
     // For other Query types, WSTE will convert to an equivalent SpanQuery.  NOT extracting position spans here.
     new WeightedSpanTermExtractor(field) {
@@ -147,10 +133,19 @@ public class PhraseHelper {
 
       // called on Query types that are NOT position sensitive, e.g. TermQuery
       @Override
-      protected void extractWeightedTerms(Map<String, WeightedSpanTerm> terms, Query query, float boost)
-          throws IOException {
-        query.createWeight(UnifiedHighlighter.EMPTY_INDEXSEARCHER, ScoreMode.COMPLETE_NO_SCORES, boost)
-            .extractTerms(extractPosInsensitiveTermsTarget);
+      protected void extractWeightedTerms(Map<String, WeightedSpanTerm> terms, Query query, float boost) {
+        query.visit(new QueryVisitor() {
+          @Override
+          public boolean acceptField(String field) {
+            return fieldMatcher.test(field);
+          }
+          @Override
+          public void consumeTerms(Query query, Term... terms) {
+            for (Term term : terms) {
+              positionInsensitiveTerms.add(term.bytes());
+            }
+          }
+        });
       }
 
       // called on SpanQueries. Some other position-sensitive queries like PhraseQuery are converted beforehand
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
index 7dcac30..f4b1ab1 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
@@ -54,8 +54,8 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.search.spans.SpanQuery;
@@ -140,11 +140,11 @@ public class UnifiedHighlighter {
   private int cacheFieldValCharsThreshold = DEFAULT_CACHE_CHARS_THRESHOLD;
 
   /**
-   * Calls {@link Weight#extractTerms(Set)} on an empty index for the query.
+   * Extracts matching terms after rewriting against an empty index
    */
   protected static Set<Term> extractTerms(Query query) throws IOException {
     Set<Term> queryTerms = new HashSet<>();
-    EMPTY_INDEXSEARCHER.createWeight(EMPTY_INDEXSEARCHER.rewrite(query), ScoreMode.COMPLETE_NO_SCORES, 1).extractTerms(queryTerms);
+    EMPTY_INDEXSEARCHER.rewrite(query).visit(QueryVisitor.termCollector(queryTerms));
     return queryTerms;
   }
 
@@ -816,7 +816,7 @@ public class UnifiedHighlighter {
         || highlightFlags.contains(HighlightFlag.WEIGHT_MATCHES); // Weight.Matches will find all
 
     return highlightFlags.contains(HighlightFlag.MULTI_TERM_QUERY)
-        ? MultiTermHighlighting.extractAutomata(query, getFieldMatcher(field), lookInSpan, this::preMultiTermQueryRewrite)
+        ? MultiTermHighlighting.extractAutomata(query, getFieldMatcher(field), lookInSpan)
         : ZERO_LEN_AUTOMATA_ARRAY;
   }
 
@@ -863,7 +863,7 @@ public class UnifiedHighlighter {
           //skip using a memory index since it's pure term filtering
           return new TokenStreamOffsetStrategy(components, getIndexAnalyzer());
         } else {
-          return new MemoryIndexOffsetStrategy(components, getIndexAnalyzer(), this::preMultiTermQueryRewrite);
+          return new MemoryIndexOffsetStrategy(components, getIndexAnalyzer());
         }
       case NONE_NEEDED:
         return NoOpOffsetStrategy.INSTANCE;
@@ -902,19 +902,6 @@ public class UnifiedHighlighter {
     return null;
   }
 
-  /**
-   * When dealing with multi term queries / span queries, we may need to handle custom queries that aren't supported
-   * by the default automata extraction in {@code MultiTermHighlighting}. This can be overridden to return a collection
-   * of queries if appropriate, or null if nothing to do. If query is not custom, simply returning null will allow the
-   * default rules to apply.
-   *
-   * @param query Query to be highlighted
-   * @return A Collection of Query object(s) if needst o be rewritten, otherwise null.
-   */
-  protected Collection<Query> preMultiTermQueryRewrite(Query query) {
-    return null;
-  }
-
   private DocIdSetIterator asDocIdSetIterator(int[] sortedDocIds) {
     return new DocIdSetIterator() {
       int idx = -1;
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/highlight/HighlighterTest.java b/lucene/highlighter/src/test/org/apache/lucene/search/highlight/HighlighterTest.java
index c52b8d3..c555297 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/highlight/HighlighterTest.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/highlight/HighlighterTest.java
@@ -72,6 +72,7 @@ import org.apache.lucene.search.PhraseQuery;
 import org.apache.lucene.search.PhraseQuery.Builder;
 import org.apache.lucene.search.PrefixQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.RegexpQuery;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
@@ -256,6 +257,11 @@ public class HighlighterTest extends BaseTokenStreamTestCase implements Formatte
       }
 
       @Override
+      public void visit(QueryVisitor visitor) {
+
+      }
+
+      @Override
       public String toString(String field) {
         return null;
       }
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/highlight/custom/HighlightCustomQueryTest.java b/lucene/highlighter/src/test/org/apache/lucene/search/highlight/custom/HighlightCustomQueryTest.java
index a54687d..2553a80 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/highlight/custom/HighlightCustomQueryTest.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/highlight/custom/HighlightCustomQueryTest.java
@@ -16,6 +16,11 @@
  */
 package org.apache.lucene.search.highlight.custom;
 
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
 import org.apache.lucene.analysis.CannedTokenStream;
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.analysis.MockTokenFilter;
@@ -25,6 +30,7 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.highlight.Highlighter;
 import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
@@ -35,11 +41,6 @@ import org.apache.lucene.search.highlight.WeightedSpanTerm;
 import org.apache.lucene.search.highlight.WeightedSpanTermExtractor;
 import org.apache.lucene.util.LuceneTestCase;
 
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Objects;
-
 /**
  * Tests the extensibility of {@link WeightedSpanTermExtractor} and
  * {@link QueryScorer} in a user defined package
@@ -176,6 +177,11 @@ public class HighlightCustomQueryTest extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.consumeTerms(this, term);
+    }
+
+    @Override
     public int hashCode() {
       return classHash() + Objects.hashCode(term);
     }
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterMTQ.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterMTQ.java
index 44d6f7b..34fd46f 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterMTQ.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterMTQ.java
@@ -21,7 +21,6 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.List;
 import java.util.Objects;
 
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
@@ -50,6 +49,7 @@ import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.PhraseQuery;
 import org.apache.lucene.search.PrefixQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.RegexpQuery;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Sort;
@@ -999,15 +999,7 @@ public class TestUnifiedHighlighterMTQ extends LuceneTestCase {
     iw.close();
 
     IndexSearcher searcher = newSearcher(ir);
-    UnifiedHighlighter highlighter = new UnifiedHighlighter(searcher, indexAnalyzer) {
-      @Override
-      protected List<Query> preMultiTermQueryRewrite(Query query) {
-        if (query instanceof MyWrapperSpanQuery) {
-          return Collections.singletonList(((MyWrapperSpanQuery) query).originalQuery);
-        }
-        return null;
-      }
-    };
+    UnifiedHighlighter highlighter = new UnifiedHighlighter(searcher, indexAnalyzer);
 
     int docId = searcher.search(new TermQuery(new Term("id", "id")), 1).scoreDocs[0].doc;
 
@@ -1052,6 +1044,11 @@ public class TestUnifiedHighlighterMTQ extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      originalQuery.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+    }
+
+    @Override
     public Query rewrite(IndexReader reader) throws IOException {
       Query newOriginalQuery = originalQuery.rewrite(reader);
       if (newOriginalQuery != originalQuery) {
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java
index a6d5d01..4c05e1b 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java
@@ -42,6 +42,7 @@ import org.apache.lucene.search.MultiPhraseQuery;
 import org.apache.lucene.search.PhraseQuery;
 import org.apache.lucene.search.PrefixQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Sort;
@@ -588,6 +589,11 @@ public class TestUnifiedHighlighterStrictPhrases extends LuceneTestCase {
     public int hashCode() {
       return wrapped.hashCode();
     }
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+      wrapped.visit(visitor);
+    }
   }
 
   // Ported from LUCENE-5455 (fixed in LUCENE-8121).  Also see LUCENE-2287.
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/vectorhighlight/FieldQueryTest.java b/lucene/highlighter/src/test/org/apache/lucene/search/vectorhighlight/FieldQueryTest.java
index eb888f6..135d8a6 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/vectorhighlight/FieldQueryTest.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/vectorhighlight/FieldQueryTest.java
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 package org.apache.lucene.search.vectorhighlight;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -29,6 +30,7 @@ import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.ConstantScoreQuery;
 import org.apache.lucene.search.PrefixQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.RegexpQuery;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.TermRangeQuery;
@@ -926,6 +928,12 @@ public class FieldQueryTest extends AbstractTestCase {
       public String toString(String field) {
         return "DummyQuery";
       }
+
+      @Override
+      public void visit(QueryVisitor visitor) {
+
+      }
+
       @Override
       public boolean equals(Object o) {
         throw new AssertionError();
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java
index 8247f81..6bfdd1b 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsQuery.java
@@ -17,7 +17,6 @@
 package org.apache.lucene.search.join;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.LeafReaderContext;
@@ -29,6 +28,7 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.TwoPhaseIterator;
 import org.apache.lucene.search.Weight;
@@ -61,6 +61,11 @@ final class GlobalOrdinalsQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
     if (searcher.getTopReaderContext().id() != indexReaderContextId) {
       throw new IllegalStateException("Creating the weight against a different index reader than this query has been built for.");
@@ -108,9 +113,6 @@ final class GlobalOrdinalsQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {}
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       SortedDocValues values = DocValues.getSorted(context.reader(), joinField);
       if (values == null) {
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java
index cdcf070..4cdf59b 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/GlobalOrdinalsWithScoreQuery.java
@@ -17,7 +17,6 @@
 package org.apache.lucene.search.join;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.LeafReaderContext;
@@ -29,6 +28,7 @@ import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.FilterWeight;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.TwoPhaseIterator;
 import org.apache.lucene.search.Weight;
@@ -66,6 +66,11 @@ final class GlobalOrdinalsWithScoreQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
     if (searcher.getTopReaderContext().id() != indexReaderContextId) {
       throw new IllegalStateException("Creating the weight against a different index reader than this query has been built for.");
@@ -126,9 +131,6 @@ final class GlobalOrdinalsWithScoreQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {}
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       SortedDocValues values = DocValues.getSorted(context.reader(), joinField);
       if (values == null) {
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/ParentChildrenBlockJoinQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/ParentChildrenBlockJoinQuery.java
index 7ce1c29..1108a49 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/ParentChildrenBlockJoinQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/ParentChildrenBlockJoinQuery.java
@@ -18,16 +18,15 @@
 package org.apache.lucene.search.join;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.ReaderUtil;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.util.BitSet;
@@ -84,6 +83,11 @@ public class ParentChildrenBlockJoinQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Query rewrite(IndexReader reader) throws IOException {
     final Query childRewrite = childQuery.rewrite(reader);
     if (childRewrite != childQuery) {
@@ -100,11 +104,6 @@ public class ParentChildrenBlockJoinQuery extends Query {
     return new Weight(this) {
 
       @Override
-      public void extractTerms(Set<Term> terms) {
-        childWeight.extractTerms(terms);
-      }
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         return Explanation.noMatch("Not implemented, use ToParentBlockJoinQuery explain why a document matched");
       }
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java
index 80810e2..9b31e34 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java
@@ -21,7 +21,6 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.function.BiFunction;
 
 import org.apache.lucene.document.DoublePoint;
@@ -36,12 +35,12 @@ import org.apache.lucene.index.PointValues.IntersectVisitor;
 import org.apache.lucene.index.PointValues.Relation;
 import org.apache.lucene.index.PrefixCodedTerms;
 import org.apache.lucene.index.PrefixCodedTerms.TermIterator;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.PointInSetQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.util.BitSetIterator;
@@ -120,14 +119,17 @@ abstract class PointInSetIncludingScoreQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public final Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
     return new Weight(this) {
 
       @Override
-      public void extractTerms(Set<Term> terms) {
-      }
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         Scorer scorer = scorer(context);
         if (scorer != null) {
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java
index 43ddd52..2466609 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java
@@ -19,17 +19,16 @@ package org.apache.lucene.search.join;
 import java.io.IOException;
 import java.util.Locale;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.util.BitSetIterator;
@@ -72,6 +71,13 @@ class TermsIncludingScoreQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(toField)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
            equalsTo(getClass().cast(other));
@@ -100,9 +106,6 @@ class TermsIncludingScoreQuery extends Query {
     return new Weight(TermsIncludingScoreQuery.this) {
 
       @Override
-      public void extractTerms(Set<Term> terms) {}
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         Terms terms = context.reader().terms(toField);
         if (terms != null) {
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/TermsQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/TermsQuery.java
index 3ff0a5c..53b9c40 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/TermsQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/TermsQuery.java
@@ -24,6 +24,7 @@ import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.util.AttributeSource;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.BytesRefHash;
@@ -60,6 +61,11 @@ class TermsQuery extends MultiTermQuery {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   protected TermsEnum getTermsEnum(Terms terms, AttributeSource atts) throws IOException {
     if (this.terms.size() == 0) {
       return TermsEnum.EMPTY;
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/ToChildBlockJoinQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/ToChildBlockJoinQuery.java
index ff8059c..11cc786 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/ToChildBlockJoinQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/ToChildBlockJoinQuery.java
@@ -20,13 +20,15 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Locale;
+
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.search.FilterWeight;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.FilterWeight;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.util.BitSet;
@@ -65,6 +67,11 @@ public class ToChildBlockJoinQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
     return new ToChildBlockJoinWeight(this, parentQuery.createWeight(searcher, scoreMode, boost), parentsFilter, scoreMode.needsScores());
   }
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinQuery.java
index 03bb5b9..c42cf39 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinQuery.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinQuery.java
@@ -31,6 +31,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Matches;
 import org.apache.lucene.search.MatchesUtils;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.ScorerSupplier;
 import org.apache.lucene.search.TwoPhaseIterator;
@@ -86,6 +87,11 @@ public class ToParentBlockJoinQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode weightScoreMode, float boost) throws IOException {
     return new BlockJoinWeight(this, childQuery.createWeight(searcher, weightScoreMode, boost), parentsFilter, weightScoreMode.needsScores() ? scoreMode : ScoreMode.None);
   }
diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java
index 1eb1adb..a82d33e 100644
--- a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java
+++ b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java
@@ -20,15 +20,14 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
@@ -63,8 +62,6 @@ import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
 
-import com.carrotsearch.randomizedtesting.generators.RandomPicks;
-
 public class TestBlockJoin extends LuceneTestCase {
 
   // One resume...
@@ -93,34 +90,6 @@ public class TestBlockJoin extends LuceneTestCase {
     return job;
   }
 
-  public void testExtractTerms() throws Exception {
-    TermQuery termQuery = new TermQuery(new Term("field", "value"));
-    QueryBitSetProducer bitSetProducer = new QueryBitSetProducer(new MatchNoDocsQuery());
-    ToParentBlockJoinQuery toParentBlockJoinQuery = new ToParentBlockJoinQuery(termQuery, bitSetProducer, ScoreMode.None);
-    ToChildBlockJoinQuery toChildBlockJoinQuery = new ToChildBlockJoinQuery(toParentBlockJoinQuery, bitSetProducer);
-
-    Directory directory = newDirectory();
-    final IndexWriter w = new IndexWriter(directory, new IndexWriterConfig(new MockAnalyzer(random())));
-    w.close();
-    IndexReader indexReader = DirectoryReader.open(directory);
-    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
-
-    Weight weight = toParentBlockJoinQuery.createWeight(indexSearcher, org.apache.lucene.search.ScoreMode.COMPLETE_NO_SCORES, 1f);
-    Set<Term> terms = new HashSet<>();
-    weight.extractTerms(terms);
-    Term[] termArr =terms.toArray(new Term[0]);
-    assertEquals(1, termArr.length);
-
-    weight = toChildBlockJoinQuery.createWeight(indexSearcher, org.apache.lucene.search.ScoreMode.COMPLETE_NO_SCORES, 1f);
-    terms = new HashSet<>();
-    weight.extractTerms(terms);
-    termArr =terms.toArray(new Term[0]);
-    assertEquals(1, termArr.length);
-
-    indexReader.close();
-    directory.close();
-  }
-
   public void testEmptyChildFilter() throws Exception {
     final Directory dir = newDirectory();
     final IndexWriterConfig config = new IndexWriterConfig(new MockAnalyzer(random()));
diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java b/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java
index 59309fa..8e5f35d 100644
--- a/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java
+++ b/lucene/join/src/test/org/apache/lucene/search/join/TestJoinUtil.java
@@ -32,6 +32,8 @@ import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
+import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.analysis.MockTokenizer;
 import org.apache.lucene.document.Document;
@@ -77,9 +79,6 @@ import org.apache.lucene.util.TestUtil;
 import org.apache.lucene.util.packed.PackedInts;
 import org.junit.Test;
 
-import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
-import com.carrotsearch.randomizedtesting.generators.RandomPicks;
-
 public class TestJoinUtil extends LuceneTestCase {
 
   public void testSimple() throws Exception {
@@ -521,10 +520,6 @@ public class TestJoinUtil extends LuceneTestCase {
           return new Weight(this) {
 
             @Override
-            public void extractTerms(Set<Term> terms) {
-            }
-
-            @Override
             public Explanation explain(LeafReaderContext context, int doc) throws IOException {
               return null;
             }
@@ -557,7 +552,12 @@ public class TestJoinUtil extends LuceneTestCase {
           };
         }
 
-        @Override
+      @Override
+      public void visit(QueryVisitor visitor) {
+
+      }
+
+      @Override
         public String toString(String field) {
           return fieldQuery.toString(field);
         }
diff --git a/lucene/misc/src/test/org/apache/lucene/search/TestDiversifiedTopDocsCollector.java b/lucene/misc/src/test/org/apache/lucene/search/TestDiversifiedTopDocsCollector.java
index db17a90..a65dac9 100644
--- a/lucene/misc/src/test/org/apache/lucene/search/TestDiversifiedTopDocsCollector.java
+++ b/lucene/misc/src/test/org/apache/lucene/search/TestDiversifiedTopDocsCollector.java
@@ -19,7 +19,6 @@ package org.apache.lucene.search;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
@@ -478,6 +477,11 @@ public class TestDiversifiedTopDocsCollector extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      query.visit(visitor);
+    }
+
+    @Override
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
       if (scoreMode.needsScores() == false) {
         return query.createWeight(searcher, scoreMode, boost);
@@ -520,12 +524,7 @@ public class TestDiversifiedTopDocsCollector extends LuceneTestCase {
             }
           };
         }
-        
-        @Override
-        public void extractTerms(Set<Term> terms) {
-          inner.extractTerms(terms);
-        }
-        
+
         @Override
         public Explanation explain(LeafReaderContext context, int doc) throws IOException {
           Scorer s = scorer(context);
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/CommonTermsQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/CommonTermsQuery.java
index 10c232e..5c361cf 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/CommonTermsQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/CommonTermsQuery.java
@@ -33,6 +33,7 @@ import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TermQuery;
 
 /**
@@ -129,7 +130,16 @@ public class CommonTermsQuery extends Query {
     collectTermStates(reader, leaves, contextArray, queryTerms);
     return buildQuery(maxDoc, contextArray, queryTerms);
   }
-  
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    Term[] selectedTerms = terms.stream().filter(t -> visitor.acceptField(t.field())).toArray(Term[]::new);
+    if (selectedTerms.length > 0) {
+      QueryVisitor v = visitor.getSubVisitor(Occur.SHOULD, this);
+      v.consumeTerms(this, selectedTerms);
+    }
+  }
+
   protected int calcLowFreqMinimumNumberShouldMatch(int numOptional) {
     return minNrShouldMatch(lowFreqMinNrShouldMatch, numOptional);
   }
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java
index 38cf3f6..39a51c1 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java
@@ -29,6 +29,7 @@ import org.apache.lucene.search.DoubleValues;
 import org.apache.lucene.search.DoubleValuesSource;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -62,6 +63,11 @@ public final class FunctionMatchQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     DoubleValuesSource vs = source.rewrite(searcher);
     return new ConstantScoreWeight(this, boost) {
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java
index f996306..c804523 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java
@@ -18,15 +18,14 @@ package org.apache.lucene.queries.function;
 
 import java.io.IOException;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -68,9 +67,6 @@ public class FunctionQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {}
-
-    @Override
     public Scorer scorer(LeafReaderContext context) throws IOException {
       return new AllScorer(context, this, boost);
     }
@@ -86,6 +82,11 @@ public class FunctionQuery extends Query {
     }
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
   protected class AllScorer extends Scorer {
     final IndexReader reader;
     final FunctionWeight weight;
@@ -150,7 +151,6 @@ public class FunctionQuery extends Query {
     return new FunctionQuery.FunctionWeight(searcher, boost);
   }
 
-
   /** Prints a user-readable version of this query. */
   @Override
   public String toString(String field)
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java
index 315f650..f72e12b 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java
@@ -19,13 +19,12 @@ package org.apache.lucene.queries.function;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 
@@ -115,6 +114,11 @@ public class FunctionRangeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new FunctionRangeWeight(searcher);
   }
@@ -130,11 +134,6 @@ public class FunctionRangeQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      //none
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       FunctionValues functionValues = valueSource.getValues(vsContext, context);
       //note: by using ValueSourceScorer directly, we avoid calling scorer.advance(doc) and checking if true,
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java
index 29d6dd9..2ba77e2 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java
@@ -19,11 +19,10 @@ package org.apache.lucene.queries.function;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.DoubleValues;
 import org.apache.lucene.search.DoubleValuesSource;
 import org.apache.lucene.search.Explanation;
@@ -31,6 +30,7 @@ import org.apache.lucene.search.FilterScorer;
 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.Weight;
@@ -120,6 +120,11 @@ public final class FunctionScoreQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    in.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+  }
+
+  @Override
   public String toString(String field) {
     return "FunctionScoreQuery(" + in.toString(field) + ", scored by " + source.toString() + ")";
   }
@@ -152,11 +157,6 @@ public final class FunctionScoreQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      this.inner.extractTerms(terms);
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       return inner.matches(context, doc);
     }
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/mlt/MoreLikeThisQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/mlt/MoreLikeThisQuery.java
index 9f3310c..976660e 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/mlt/MoreLikeThisQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/mlt/MoreLikeThisQuery.java
@@ -16,17 +16,18 @@
  */
 package org.apache.lucene.queries.mlt;
 
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.Query;
-
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.Arrays;
-import java.util.Set;
-import java.util.Objects;
+import org.apache.lucene.search.QueryVisitor;
 
 /**
  * A simple wrapper for MoreLikeThis for use in scenarios where a Query object is required eg
@@ -179,4 +180,9 @@ public class MoreLikeThisQuery extends Query {
            Arrays.equals(moreLikeFields, other.moreLikeFields) &&
            Objects.equals(stopWords, other.stopWords);
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/payloads/PayloadScoreQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/payloads/PayloadScoreQuery.java
index bd5d927..f2bf4e7 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/payloads/PayloadScoreQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/payloads/PayloadScoreQuery.java
@@ -19,17 +19,18 @@ package org.apache.lucene.queries.payloads;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LeafSimScorer;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.spans.FilterSpans;
 import org.apache.lucene.search.spans.SpanCollector;
@@ -86,6 +87,11 @@ public class PayloadScoreQuery extends SpanQuery {
     return super.rewrite(reader);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    wrappedQuery.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+  }
+
 
   @Override
   public String toString(String field) {
@@ -160,11 +166,6 @@ public class PayloadScoreQuery extends SpanQuery {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      innerWeight.extractTerms(terms);
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       PayloadSpanScorer scorer = (PayloadSpanScorer)scorer(context);
       if (scorer == null || scorer.iterator().advance(doc) != doc)
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/payloads/SpanPayloadCheckQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/payloads/SpanPayloadCheckQuery.java
index a9d3bfb..30d18f0 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/payloads/SpanPayloadCheckQuery.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/payloads/SpanPayloadCheckQuery.java
@@ -15,11 +15,11 @@
  * limitations under the License.
  */
 package org.apache.lucene.queries.payloads;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
@@ -27,9 +27,11 @@ import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.Terms;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LeafSimScorer;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.spans.FilterSpans;
 import org.apache.lucene.search.spans.FilterSpans.AcceptStatus;
@@ -77,6 +79,13 @@ public class SpanPayloadCheckQuery extends SpanQuery {
     return super.rewrite(reader);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(match.getField())) {
+      match.visit(visitor.getSubVisitor(BooleanClause.Occur.MUST, this));
+    }
+  }
+
   /**
    * Weight that pulls its Spans using a PayloadSpanCollector
    */
@@ -90,11 +99,6 @@ public class SpanPayloadCheckQuery extends SpanQuery {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      matchWeight.extractTerms(terms);
-    }
-
-    @Override
     public void extractTermStates(Map<Term, TermStates> contexts) {
       matchWeight.extractTermStates(contexts);
     }
diff --git a/lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java b/lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java
index ffe0066..9a4043d 100644
--- a/lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java
+++ b/lucene/queryparser/src/java/org/apache/lucene/queryparser/complexPhrase/ComplexPhraseQueryParser.java
@@ -36,6 +36,7 @@ import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.MultiTermQuery.RewriteMethod;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.SynonymQuery;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.spans.SpanBoostQuery;
@@ -254,6 +255,11 @@ public class ComplexPhraseQueryParser extends QueryParser {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public Query rewrite(IndexReader reader) throws IOException {
       final Query contents = this.contents[0];
       // ArrayList spanClauses = new ArrayList();
diff --git a/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/DistanceRewriteQuery.java b/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/DistanceRewriteQuery.java
index 1972a8e..adbbd0a 100644
--- a/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/DistanceRewriteQuery.java
+++ b/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/DistanceRewriteQuery.java
@@ -15,10 +15,12 @@
  * limitations under the License.
  */
 package org.apache.lucene.queryparser.surround.query;
+
 import java.io.IOException;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 
 class DistanceRewriteQuery extends RewriteQuery<DistanceQuery> {
 
@@ -33,5 +35,11 @@ class DistanceRewriteQuery extends RewriteQuery<DistanceQuery> {
   public Query rewrite(IndexReader reader) throws IOException {
     return srndQuery.getSpanNearQuery(reader, fieldName, qf);
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    // TODO implement this
+    visitor.visitLeaf(this);
+  }
 }
 
diff --git a/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/SimpleTermRewriteQuery.java b/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/SimpleTermRewriteQuery.java
index 3e0e4aa..50203a8 100644
--- a/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/SimpleTermRewriteQuery.java
+++ b/lucene/queryparser/src/java/org/apache/lucene/queryparser/surround/query/SimpleTermRewriteQuery.java
@@ -15,15 +15,17 @@
  * limitations under the License.
  */
 package org.apache.lucene.queryparser.surround.query;
+
 import java.io.IOException;
-import java.util.List;
 import java.util.ArrayList;
+import java.util.List;
 
 import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.search.QueryVisitor;
 
 class SimpleTermRewriteQuery extends RewriteQuery<SimpleTerm> {
 
@@ -50,5 +52,11 @@ class SimpleTermRewriteQuery extends RewriteQuery<SimpleTerm> {
       /* luceneSubQueries all have default weight */
       luceneSubQueries, BooleanClause.Occur.SHOULD); /* OR the subquery terms */
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    // TODO: implement this
+    visitor.visitLeaf(this);
+  }
 }
 
diff --git a/lucene/queryparser/src/test/org/apache/lucene/queryparser/classic/TestMultiAnalyzer.java b/lucene/queryparser/src/test/org/apache/lucene/queryparser/classic/TestMultiAnalyzer.java
index 85a5f39..15dd3c5 100644
--- a/lucene/queryparser/src/test/org/apache/lucene/queryparser/classic/TestMultiAnalyzer.java
+++ b/lucene/queryparser/src/test/org/apache/lucene/queryparser/classic/TestMultiAnalyzer.java
@@ -19,12 +19,18 @@ package org.apache.lucene.queryparser.classic;
 import java.io.IOException;
 import java.util.Objects;
 
-import org.apache.lucene.analysis.*;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.BaseTokenStreamTestCase;
+import org.apache.lucene.analysis.MockTokenizer;
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.Tokenizer;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
 import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 
 /**
  * Test QueryParser's ability to deal with Analyzers that return more
@@ -263,7 +269,12 @@ public class TestMultiAnalyzer extends BaseTokenStreamTestCase {
             return q.toString(f);
         }
 
-        @Override
+      @Override
+      public void visit(QueryVisitor visitor) {
+        q.visit(visitor);
+      }
+
+      @Override
         public boolean equals(Object other) {
             return sameClassAs(other) &&
                    Objects.equals(q, ((DumbQueryWrapper) other).q);
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java
index 538f4a3..124a7a7 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java
@@ -31,6 +31,7 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.ScorerSupplier;
@@ -86,6 +87,13 @@ abstract class LatLonShapeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
 
     return new ConstantScoreWeight(this, boost) {
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/queries/FuzzyLikeThisQuery.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/queries/FuzzyLikeThisQuery.java
index a7898f7..a0e1b5d 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/queries/FuzzyLikeThisQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/queries/FuzzyLikeThisQuery.java
@@ -41,6 +41,7 @@ import org.apache.lucene.search.ConstantScoreQuery;
 import org.apache.lucene.search.FuzzyTermsEnum;
 import org.apache.lucene.search.MaxNonCompetitiveBoostAttribute;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.similarities.ClassicSimilarity;
 import org.apache.lucene.search.similarities.TFIDFSimilarity;
@@ -273,6 +274,11 @@ public class FuzzyLikeThisQuery extends Query
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Query rewrite(IndexReader reader) throws IOException
   {
     ScoreTermQueue q = new ScoreTermQueue(maxNumTerms);
@@ -330,7 +336,7 @@ public class FuzzyLikeThisQuery extends Query
     // booleans with a minimum-should-match of NumFields-1?
     return bq.build();
   }
-    
+
   //Holds info for a fuzzy term variant - initially score is set to edit distance (for ranking best
   // term variants) then is reset with IDF for use in ranking against all other
   // terms/fields
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/BM25FQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/BM25FQuery.java
index 1a80351..080537b 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/BM25FQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/BM25FQuery.java
@@ -223,6 +223,15 @@ public final class BM25FQuery extends Query {
     return this;
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    Term[] selectedTerms = Arrays.stream(fieldTerms).filter(t -> visitor.acceptField(t.field())).toArray(Term[]::new);
+    if (selectedTerms.length > 0) {
+      QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+      v.consumeTerms(this, selectedTerms);
+    }
+  }
+
   private BooleanQuery rewriteToBoolean() {
     // rewrite to a simple disjunction if the score is not needed.
     BooleanQuery.Builder bq = new BooleanQuery.Builder();
@@ -291,11 +300,6 @@ public final class BM25FQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> termSet) {
-      termSet.addAll(Arrays.asList(fieldTerms));
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       Weight weight = searcher.rewrite(rewriteToBoolean()).createWeight(searcher, ScoreMode.COMPLETE, 1f);
       return weight.matches(context, doc);
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/CoveringQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/CoveringQuery.java
index fd89888..5d5c8f8 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/CoveringQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/CoveringQuery.java
@@ -21,12 +21,10 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 
 /** A {@link Query} that allows to have a configurable number or required
  *  matches per document. This is typically useful in order to build queries
@@ -110,6 +108,14 @@ public final class CoveringQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+    for (Query query : queries) {
+      query.visit(v);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     final List<Weight> weights = new ArrayList<>(queries.size());
     for (Query query : queries) {
@@ -130,13 +136,6 @@ public final class CoveringQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      for (Weight weight : weights) {
-        weight.extractTerms(terms);
-      }
-    }
-
-    @Override
     public Matches matches(LeafReaderContext context, int doc) throws IOException {
       LongValues minMatchValues = minimumNumberMatch.getValues(context, null);
       if (minMatchValues.advanceExact(doc) == false) {
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesNumbersQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesNumbersQuery.java
index f018df4..cdfe23d 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesNumbersQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesNumbersQuery.java
@@ -78,6 +78,13 @@ public class DocValuesNumbersQuery extends Query {
     return 31 * classHash() + Objects.hash(field, numbers);
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
   public String getField() {
     return field;
   }
@@ -129,4 +136,5 @@ public class DocValuesNumbersQuery extends Query {
 
     };
   }
+
 }
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesTermsQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesTermsQuery.java
index 8ca53b7..ef67b02 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesTermsQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/DocValuesTermsQuery.java
@@ -26,9 +26,9 @@ import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PrefixCodedTerms;
+import org.apache.lucene.index.PrefixCodedTerms.TermIterator;
 import org.apache.lucene.index.SortedSetDocValues;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.index.PrefixCodedTerms.TermIterator;
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.FixedBitSet;
@@ -165,6 +165,13 @@ public class DocValuesTermsQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new ConstantScoreWeight(this, boost) {
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/TermAutomatonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/TermAutomatonQuery.java
index 81426a7..cd123af 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/TermAutomatonQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/TermAutomatonQuery.java
@@ -21,7 +21,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexReaderContext;
@@ -29,8 +28,8 @@ import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.ReaderUtil;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.similarities.Similarity;
 import org.apache.lucene.search.spans.SpanNearQuery;
@@ -363,15 +362,6 @@ public class TermAutomatonQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      for(BytesRef text : termToID.keySet()) {
-        if (text != null) {
-          terms.add(new Term(field, text));
-        }
-      }
-    }
-
-    @Override
     public String toString() {
       return "weight(" + TermAutomatonQuery.this + ")";
     }
@@ -492,4 +482,15 @@ public class TermAutomatonQuery extends Query {
     // TODO: we could maybe also rewrite to union of PhraseQuery (pull all finite strings) if it's "worth it"?
     return this;
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field) == false) {
+      return;
+    }
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, this);
+    for (BytesRef term : termToID.keySet()) {
+      v.consumeTerms(this, new Term(field, term));
+    }
+  }
 }
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ConjunctionIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ConjunctionIntervalsSource.java
index ec4341d..123cc38 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ConjunctionIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ConjunctionIntervalsSource.java
@@ -21,15 +21,15 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.FilterMatchesIterator;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.MatchesUtils;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 
 class ConjunctionIntervalsSource extends IntervalsSource {
 
@@ -56,9 +56,11 @@ class ConjunctionIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
+  public void visit(String field, QueryVisitor visitor) {
+    Query parent = new IntervalQuery(field, this);
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.MUST, parent);
     for (IntervalsSource source : subSources) {
-      source.extractTerms(field, terms);
+      source.visit(field, v);
     }
   }
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DifferenceIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DifferenceIntervalsSource.java
index 7289d04..f1d096c 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DifferenceIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DifferenceIntervalsSource.java
@@ -19,11 +19,11 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.MatchesIterator;
+import org.apache.lucene.search.QueryVisitor;
 
 class DifferenceIntervalsSource extends IntervalsSource {
 
@@ -83,8 +83,10 @@ class DifferenceIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
-    minuend.extractTerms(field, terms);
+  public void visit(String field, QueryVisitor visitor) {
+    IntervalQuery q = new IntervalQuery(field, this);
+    minuend.visit(field, visitor.getSubVisitor(BooleanClause.Occur.MUST, q));
+    subtrahend.visit(field, visitor.getSubVisitor(BooleanClause.Occur.MUST_NOT, q));
   }
 
   @Override
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java
index b280885..f37dcb6 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java
@@ -21,14 +21,15 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.MatchesUtils;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.util.PriorityQueue;
 
 class DisjunctionIntervalsSource extends IntervalsSource {
@@ -84,9 +85,11 @@ class DisjunctionIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
+  public void visit(String field, QueryVisitor visitor) {
+    Query parent = new IntervalQuery(field, this);
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, parent);
     for (IntervalsSource source : subSources) {
-      source.extractTerms(field, terms);
+      source.visit(field, v);
     }
   }
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ExtendedIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ExtendedIntervalsSource.java
index 864a4b5..41a8829 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ExtendedIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/ExtendedIntervalsSource.java
@@ -19,11 +19,10 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.MatchesIterator;
+import org.apache.lucene.search.QueryVisitor;
 
 class ExtendedIntervalsSource extends IntervalsSource {
 
@@ -57,8 +56,8 @@ class ExtendedIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
-    source.extractTerms(field, terms);
+  public void visit(String field, QueryVisitor visitor) {
+    source.visit(field, visitor);
   }
 
   @Override
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FilteredIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FilteredIntervalsSource.java
index c2b4d60..b334129 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FilteredIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FilteredIntervalsSource.java
@@ -19,11 +19,10 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.MatchesIterator;
+import org.apache.lucene.search.QueryVisitor;
 
 /**
  * An IntervalsSource that filters the intervals from another IntervalsSource
@@ -83,8 +82,8 @@ public abstract class FilteredIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
-    in.extractTerms(field, terms);
+  public void visit(String field, QueryVisitor visitor) {
+    in.visit(field, visitor);
   }
 
   @Override
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FixedFieldIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FixedFieldIntervalsSource.java
index 7776a2b..ab24ee3 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FixedFieldIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/FixedFieldIntervalsSource.java
@@ -19,11 +19,10 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.MatchesIterator;
+import org.apache.lucene.search.QueryVisitor;
 
 class FixedFieldIntervalsSource extends IntervalsSource {
 
@@ -46,8 +45,8 @@ class FixedFieldIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
-    source.extractTerms(this.field, terms);
+  public void visit(String field, QueryVisitor visitor) {
+    source.visit(this.field, visitor);
   }
 
   @Override
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalQuery.java
index 62fe067..6b6450d 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalQuery.java
@@ -19,10 +19,8 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.FilterMatchesIterator;
 import org.apache.lucene.search.IndexSearcher;
@@ -30,6 +28,7 @@ import org.apache.lucene.search.Matches;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.MatchesUtils;
 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.Weight;
@@ -121,6 +120,13 @@ public final class IntervalQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      intervalsSource.visit(field, visitor);
+    }
+  }
+
+  @Override
   public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
@@ -146,11 +152,6 @@ public final class IntervalQuery extends Query {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      intervalsSource.extractTerms(field, terms);
-    }
-
-    @Override
     public Explanation explain(LeafReaderContext context, int doc) throws IOException {
       IntervalScorer scorer = (IntervalScorer) scorer(context);
       if (scorer != null) {
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalsSource.java
index dc4161f..1c49d24 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalsSource.java
@@ -18,11 +18,10 @@
 package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.MatchesIterator;
+import org.apache.lucene.search.QueryVisitor;
 
 /**
  * A helper class for {@link IntervalQuery} that provides an {@link IntervalIterator}
@@ -56,11 +55,9 @@ public abstract class IntervalsSource {
   public abstract MatchesIterator matches(String field, LeafReaderContext ctx, int doc) throws IOException;
 
   /**
-   * Expert: collect {@link Term} objects from this source
-   * @param field the field to be scored
-   * @param terms a {@link Set} which terms should be added to
+   * Expert: visit the tree of sources
    */
-  public abstract void extractTerms(String field, Set<Term> terms);
+  public abstract void visit(String field, QueryVisitor visitor);
 
   /**
    * Return the minimum possible width of an interval returned by this source
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MinimumShouldMatchIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MinimumShouldMatchIntervalsSource.java
index 1935c62..a06c478 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MinimumShouldMatchIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MinimumShouldMatchIntervalsSource.java
@@ -25,15 +25,15 @@ import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.MatchesUtils;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.util.PriorityQueue;
 
 class MinimumShouldMatchIntervalsSource extends IntervalsSource {
@@ -85,9 +85,11 @@ class MinimumShouldMatchIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
+  public void visit(String field, QueryVisitor visitor) {
+    Query parent = new IntervalQuery(field, this);
+    QueryVisitor v = visitor.getSubVisitor(BooleanClause.Occur.SHOULD, parent);
     for (IntervalsSource source : sources) {
-      source.extractTerms(field, terms);
+      source.visit(field, v);
     }
   }
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java
index 7689d1d..1ee52d4 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java
@@ -21,14 +21,13 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.MatchesUtils;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.automaton.CompiledAutomaton;
 
@@ -89,7 +88,7 @@ class MultiTermIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
+  public void visit(String field, QueryVisitor visitor) {
 
   }
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/OffsetIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/OffsetIntervalsSource.java
index b2ca302..078c64b 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/OffsetIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/OffsetIntervalsSource.java
@@ -19,11 +19,10 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.MatchesIterator;
+import org.apache.lucene.search.QueryVisitor;
 
 /**
  * Tracks a reference intervals source, and produces a pseudo-interval that appears
@@ -144,8 +143,8 @@ class OffsetIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
-    in.extractTerms(field, terms);
+  public void visit(String field, QueryVisitor visitor) {
+    in.visit(field, visitor);
   }
 
   @Override
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/TermIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/TermIntervalsSource.java
index 4539d2f..3d6fe20 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/TermIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/TermIntervalsSource.java
@@ -19,7 +19,6 @@ package org.apache.lucene.search.intervals;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat;
 import org.apache.lucene.codecs.lucene50.Lucene50PostingsReader;
@@ -32,6 +31,7 @@ import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.MatchesIterator;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TwoPhaseIterator;
 import org.apache.lucene.util.BytesRef;
 
@@ -227,8 +227,8 @@ class TermIntervalsSource extends IntervalsSource {
   }
 
   @Override
-  public void extractTerms(String field, Set<Term> terms) {
-    terms.add(new Term(field, term));
+  public void visit(String field, QueryVisitor visitor) {
+    visitor.consumeTerms(new IntervalQuery(field, this), new Term(field, term));
   }
 
   /** A guess of
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/queries/FuzzyLikeThisQueryTest.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/queries/FuzzyLikeThisQueryTest.java
index ead2720..95c6cca 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/queries/FuzzyLikeThisQueryTest.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/queries/FuzzyLikeThisQueryTest.java
@@ -16,6 +16,9 @@
  */
 package org.apache.lucene.sandbox.queries;
 
+import java.io.IOException;
+import java.util.HashSet;
+
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.document.Document;
@@ -25,16 +28,13 @@ import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.IOUtils;
 import org.apache.lucene.util.LuceneTestCase;
 
-import java.io.IOException;
-import java.util.HashSet;
-
 public class FuzzyLikeThisQueryTest extends LuceneTestCase {
   private Directory directory;
   private IndexSearcher searcher;
@@ -81,7 +81,7 @@ public class FuzzyLikeThisQueryTest extends LuceneTestCase {
     flt.addTerms("smith", "name", 2, 1);
     Query q = flt.rewrite(searcher.getIndexReader());
     HashSet<Term> queryTerms = new HashSet<>();
-    searcher.createWeight(q, ScoreMode.COMPLETE, 1f).extractTerms(queryTerms);
+    q.visit(QueryVisitor.termCollector(queryTerms));
     assertTrue("Should have variant smythe", queryTerms.contains(new Term("name", "smythe")));
     assertTrue("Should have variant smith", queryTerms.contains(new Term("name", "smith")));
     assertTrue("Should have variant smyth", queryTerms.contains(new Term("name", "smyth")));
@@ -98,7 +98,7 @@ public class FuzzyLikeThisQueryTest extends LuceneTestCase {
     flt.addTerms("jonathin smoth", "name", 2, 1);
     Query q = flt.rewrite(searcher.getIndexReader());
     HashSet<Term> queryTerms = new HashSet<>();
-    searcher.createWeight(q, ScoreMode.COMPLETE, 1f).extractTerms(queryTerms);
+    q.visit(QueryVisitor.termCollector(queryTerms));
     assertTrue("Should have variant jonathan", queryTerms.contains(new Term("name", "jonathan")));
     assertTrue("Should have variant smith", queryTerms.contains(new Term("name", "smith")));
     TopDocs topDocs = searcher.search(flt, 1);
@@ -116,7 +116,7 @@ public class FuzzyLikeThisQueryTest extends LuceneTestCase {
     // don't fail here just because the field doesn't exits
     Query q = flt.rewrite(searcher.getIndexReader());
     HashSet<Term> queryTerms = new HashSet<>();
-    searcher.createWeight(q, ScoreMode.COMPLETE, 1f).extractTerms(queryTerms);
+    q.visit(QueryVisitor.termCollector(queryTerms));
     assertTrue("Should have variant jonathan", queryTerms.contains(new Term("name", "jonathan")));
     assertTrue("Should have variant smith", queryTerms.contains(new Term("name", "smith")));
     TopDocs topDocs = searcher.search(flt, 1);
@@ -133,7 +133,7 @@ public class FuzzyLikeThisQueryTest extends LuceneTestCase {
     flt.addTerms("fernando smith", "name", 2, 1);
     Query q = flt.rewrite(searcher.getIndexReader());
     HashSet<Term> queryTerms = new HashSet<>();
-    searcher.createWeight(q, ScoreMode.COMPLETE, 1f).extractTerms(queryTerms);
+    q.visit(QueryVisitor.termCollector(queryTerms));
     assertTrue("Should have variant smith", queryTerms.contains(new Term("name", "smith")));
     TopDocs topDocs = searcher.search(flt, 1);
     ScoreDoc[] sd = topDocs.scoreDocs;
diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/TestTermAutomatonQuery.java b/lucene/sandbox/src/test/org/apache/lucene/search/TestTermAutomatonQuery.java
index 0d7ce58..3aaa3ed 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/search/TestTermAutomatonQuery.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/search/TestTermAutomatonQuery.java
@@ -606,6 +606,11 @@ public class TestTermAutomatonQuery extends LuceneTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return "RandomFilter(seed=" + seed + ",density=" + density + ")";
     }
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeVerifyQuery.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeVerifyQuery.java
index cadacdc..e8149cb 100644
--- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeVerifyQuery.java
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeVerifyQuery.java
@@ -24,6 +24,7 @@ import org.apache.lucene.search.ConstantScoreScorer;
 import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -81,6 +82,11 @@ public class CompositeVerifyQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     final Weight indexQueryWeight = indexQuery.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, boost);//scores aren't unsupported
 
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/IntersectsRPTVerifyQuery.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/IntersectsRPTVerifyQuery.java
index 1bcb7b5..7fa9864 100644
--- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/IntersectsRPTVerifyQuery.java
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/IntersectsRPTVerifyQuery.java
@@ -25,6 +25,7 @@ import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -81,6 +82,11 @@ public class IntersectsRPTVerifyQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
 
     return new ConstantScoreWeight(this, boost) {
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeQuery.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeQuery.java
index a5171c3..943aa31 100644
--- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeQuery.java
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeQuery.java
@@ -29,6 +29,7 @@ import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -77,6 +78,13 @@ public abstract class AbstractPrefixTreeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(fieldName)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new ConstantScoreWeight(this, boost) {
       @Override
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/serialized/SerializedDVStrategy.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/serialized/SerializedDVStrategy.java
index ff019c0..da70f0d 100644
--- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/serialized/SerializedDVStrategy.java
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/serialized/SerializedDVStrategy.java
@@ -34,6 +34,7 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.DoubleValuesSource;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -152,6 +153,11 @@ public class SerializedDVStrategy extends SpatialStrategy {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public boolean equals(Object other) {
       return sameClassAs(other) &&
              predicateValueSource.equals(((PredicateValueSourceQuery) other).predicateValueSource);
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java
index 9de43d1..6f2cb82 100644
--- a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/vector/PointVectorStrategy.java
@@ -37,6 +37,7 @@ import org.apache.lucene.search.DoubleValues;
 import org.apache.lucene.search.DoubleValuesSource;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -266,6 +267,11 @@ public class PointVectorStrategy extends SpatialStrategy {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
       Weight w = inner.createWeight(searcher, scoreMode, 1f);
       return new ConstantScoreWeight(this, boost) {
diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java
index 8fcf056..6b0f788 100644
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/PointInGeo3DShapeQuery.java
@@ -25,6 +25,7 @@ import org.apache.lucene.search.ConstantScoreScorer;
 import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -61,6 +62,13 @@ final class PointInGeo3DShapeQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
 
     // I don't use RandomAccessWeight here: it's no good to approximate with "match all docs"; this is an inverted structure and should be
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionWeight.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionWeight.java
index 1f97d55..8632c22 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionWeight.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionWeight.java
@@ -17,11 +17,9 @@
 package org.apache.lucene.search.suggest.document;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.search.BulkScorer;
 import org.apache.lucene.search.Explanation;
@@ -143,11 +141,6 @@ public class CompletionWeight extends Weight {
   }
 
   @Override
-  public void extractTerms(Set<Term> terms) {
-    // no-op
-  }
-
-  @Override
   public Explanation explain(LeafReaderContext context, int doc) throws IOException {
     //TODO
     return null;
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/ContextQuery.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/ContextQuery.java
index 1a2680c..bb2131d 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/ContextQuery.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/ContextQuery.java
@@ -24,6 +24,7 @@ import java.util.TreeSet;
 
 import org.apache.lucene.analysis.miscellaneous.ConcatenateGraphFilter;
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.util.BytesRef;
@@ -336,4 +337,9 @@ public class ContextQuery extends CompletionQuery {
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
 }
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/PrefixCompletionQuery.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/PrefixCompletionQuery.java
index a8da150..896d9c8 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/PrefixCompletionQuery.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/PrefixCompletionQuery.java
@@ -21,6 +21,7 @@ import java.io.IOException;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.search.suggest.BitsProducer;
@@ -73,6 +74,10 @@ public class PrefixCompletionQuery extends CompletionQuery {
     }
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
   /**
    * Gets the analyzer used to analyze the prefix.
    */
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/RegexCompletionQuery.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/RegexCompletionQuery.java
index 6fa547a..4487f22 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/RegexCompletionQuery.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/RegexCompletionQuery.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.search.suggest.BitsProducer;
@@ -123,4 +124,9 @@ public class RegexCompletionQuery extends CompletionQuery {
   public int hashCode() {
     throw new UnsupportedOperationException();
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/AssertingQuery.java b/lucene/test-framework/src/java/org/apache/lucene/search/AssertingQuery.java
index b3d2f81..8989106 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/AssertingQuery.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/AssertingQuery.java
@@ -78,4 +78,9 @@ public final class AssertingQuery extends Query {
     }
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    in.visit(visitor);
+  }
+
 }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/BlockScoreQueryWrapper.java b/lucene/test-framework/src/java/org/apache/lucene/search/BlockScoreQueryWrapper.java
index b15fa28..cfebc6a 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/BlockScoreQueryWrapper.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/BlockScoreQueryWrapper.java
@@ -19,11 +19,9 @@ package org.apache.lucene.search;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.util.ArrayUtil;
 
 /**
@@ -73,6 +71,11 @@ public final class BlockScoreQueryWrapper extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    query.visit(visitor);
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     final Weight inWeight = query.createWeight(searcher, scoreMode, boost);
     if (scoreMode.needsScores() == false) {
@@ -198,11 +201,6 @@ public final class BlockScoreQueryWrapper extends Query {
       }
 
       @Override
-      public void extractTerms(Set<Term> terms) {
-        inWeight.extractTerms(terms);
-      }
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         return inWeight.explain(context, doc);
       }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/QueryUtils.java b/lucene/test-framework/src/java/org/apache/lucene/search/QueryUtils.java
index 840e78e..cdbac77 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/QueryUtils.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/QueryUtils.java
@@ -67,6 +67,9 @@ public class QueryUtils {
       }
 
       @Override
+      public void visit(QueryVisitor visitor) { }
+
+      @Override
       public boolean equals(Object o) {
         return o == this;
       }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/RandomApproximationQuery.java b/lucene/test-framework/src/java/org/apache/lucene/search/RandomApproximationQuery.java
index b43ccc4..445a39a 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/RandomApproximationQuery.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/RandomApproximationQuery.java
@@ -18,8 +18,8 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import java.util.Random;
-import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
 
+import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 
@@ -46,6 +46,11 @@ public class RandomApproximationQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    query.visit(visitor);
+  }
+
+  @Override
   public boolean equals(Object other) {
     return sameClassAs(other) &&
            query.equals(((RandomApproximationQuery) other).query);
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/ShardSearchingTestBase.java b/lucene/test-framework/src/java/org/apache/lucene/search/ShardSearchingTestBase.java
index 1b81b77..27d4e6c 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/ShardSearchingTestBase.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/ShardSearchingTestBase.java
@@ -231,9 +231,8 @@ public abstract class ShardSearchingTestBase extends LuceneTestCase {
       public Query rewrite(Query original) throws IOException {
         final IndexSearcher localSearcher = new IndexSearcher(getIndexReader());
         original = localSearcher.rewrite(original);
-        final Weight weight = localSearcher.createWeight(original, ScoreMode.COMPLETE, 1);
         final Set<Term> terms = new HashSet<>();
-        weight.extractTerms(terms);
+        original.visit(QueryVisitor.termCollector(terms));
 
         // Make a single request to remote nodes for term
         // stats:
@@ -259,7 +258,7 @@ public abstract class ShardSearchingTestBase extends LuceneTestCase {
           }
         }
 
-        return weight.getQuery();
+        return original;
       }
 
       @Override
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanQuery.java b/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanQuery.java
index f24a4ff..1040a3a 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanQuery.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanQuery.java
@@ -16,14 +16,15 @@
  */
 package org.apache.lucene.search.spans;
 
+import java.io.IOException;
+import java.util.Objects;
+
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 
-import java.io.IOException;
-import java.util.Objects;
-
 /** Wraps a span query with asserts */
 public class AssertingSpanQuery extends SpanQuery {
   private final SpanQuery in;
@@ -61,6 +62,11 @@ public class AssertingSpanQuery extends SpanQuery {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    in.visit(visitor);
+  }
+
+  @Override
   public Query clone() {
     return new AssertingSpanQuery(in);
   }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanWeight.java b/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanWeight.java
index be04e00..9c73a43 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanWeight.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/search/spans/AssertingSpanWeight.java
@@ -18,7 +18,6 @@ package org.apache.lucene.search.spans;
 
 import java.io.IOException;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
@@ -61,12 +60,6 @@ public class AssertingSpanWeight extends SpanWeight {
   public LeafSimScorer getSimScorer(LeafReaderContext context) throws IOException {
     return in.getSimScorer(context);
   }
-
-  @Override
-  public void extractTerms(Set<Term> terms) {
-    in.extractTerms(terms);
-  }
-
   @Override
   public SpanScorer scorer(LeafReaderContext context) throws IOException {
     return in.scorer(context);
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/LTRScoringQuery.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/LTRScoringQuery.java
index 332e1b2..811e940 100644
--- a/solr/contrib/ltr/src/java/org/apache/solr/ltr/LTRScoringQuery.java
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/LTRScoringQuery.java
@@ -24,7 +24,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
@@ -32,7 +31,6 @@ import java.util.concurrent.RunnableFuture;
 import java.util.concurrent.Semaphore;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DisiPriorityQueue;
 import org.apache.lucene.search.DisiWrapper;
 import org.apache.lucene.search.DisjunctionDISIApproximation;
@@ -40,6 +38,7 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -153,6 +152,11 @@ public class LTRScoringQuery extends Query {
     return sameClassAs(o) &&  equalsTo(getClass().cast(o));
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
   private boolean equalsTo(LTRScoringQuery other) {
     if (ltrScoringModel == null) {
       if (other.ltrScoringModel != null) {
@@ -443,13 +447,6 @@ public class LTRScoringQuery extends Query {
 
     }
 
-    @Override
-    public void extractTerms(Set<Term> terms) {
-      for (final Feature.FeatureWeight feature : extractedFeatureWeights) {
-        feature.extractTerms(terms);
-      }
-    }
-
     protected void reset() {
       for (int i = 0; i < extractedFeatureWeights.length;++i){
         int featId = extractedFeatureWeights[i].getIndex();
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/Feature.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/Feature.java
index 066d281..c6e545b 100644
--- a/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/Feature.java
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/Feature.java
@@ -19,14 +19,13 @@ package org.apache.solr.ltr.feature;
 import java.io.IOException;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.Weight;
 import org.apache.solr.core.SolrResourceLoader;
@@ -134,6 +133,11 @@ public abstract class Feature extends Query {
     return sameClassAs(o) &&  equalsTo(getClass().cast(o));
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
   private boolean equalsTo(Feature other) {
     if (index != other.index) {
       return false;
@@ -261,11 +265,6 @@ public abstract class Feature extends Query {
       return Feature.this.toString();
     }
 
-    @Override
-    public void extractTerms(Set<Term> terms) {
-      // no-op
-    }
-
     /**
      * A 'recipe' for computing a feature
      */
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/OriginalScoreFeature.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/OriginalScoreFeature.java
index 7a7770e..fc2fb90 100644
--- a/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/OriginalScoreFeature.java
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/OriginalScoreFeature.java
@@ -19,10 +19,8 @@ package org.apache.solr.ltr.feature;
 import java.io.IOException;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
@@ -80,11 +78,6 @@ public class OriginalScoreFeature extends Feature {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      w.extractTerms(terms);
-    }
-
-    @Override
     public FeatureScorer scorer(LeafReaderContext context) throws IOException {
 
       final Scorer originalScorer = w.scorer(context);
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/SolrFeature.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/SolrFeature.java
index 7866ecc..d9851f5 100644
--- a/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/SolrFeature.java
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/feature/SolrFeature.java
@@ -21,10 +21,8 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
@@ -210,13 +208,6 @@ public class SolrFeature extends Feature {
     }
 
     @Override
-    public void extractTerms(Set<Term> terms) {
-      if (solrQueryWeight != null) {
-        solrQueryWeight.extractTerms(terms);
-      }
-    }
-
-    @Override
     public FeatureScorer scorer(LeafReaderContext context) throws IOException {
       Scorer solrScorer = null;
       if (solrQueryWeight != null) {
diff --git a/solr/core/src/java/org/apache/solr/legacy/LegacyNumericRangeQuery.java b/solr/core/src/java/org/apache/solr/legacy/LegacyNumericRangeQuery.java
index d07e497..e39cfb7 100644
--- a/solr/core/src/java/org/apache/solr/legacy/LegacyNumericRangeQuery.java
+++ b/solr/core/src/java/org/apache/solr/legacy/LegacyNumericRangeQuery.java
@@ -27,16 +27,17 @@ import org.apache.lucene.document.IntPoint;
 import org.apache.lucene.document.LongPoint;
 import org.apache.lucene.index.FilteredTermsEnum;
 import org.apache.lucene.index.PointValues;
+import org.apache.lucene.index.Term;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.TermRangeQuery;
 import org.apache.lucene.util.AttributeSource;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
-import org.apache.lucene.index.Term; // for javadocs
 
 /**
  * <p>A {@link Query} that matches numeric values within a
@@ -533,5 +534,10 @@ public final class LegacyNumericRangeQuery<T extends Number> extends MultiTermQu
     }
 
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
   
 }
diff --git a/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java b/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java
index a97bae5..0019ed6 100644
--- a/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java
+++ b/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java
@@ -41,6 +41,7 @@ import org.apache.lucene.search.MultiPhraseQuery;
 import org.apache.lucene.search.MultiTermQuery;
 import org.apache.lucene.search.PhraseQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.RegexpQuery;
 import org.apache.lucene.search.WildcardQuery;
 import org.apache.lucene.util.QueryBuilder;
@@ -61,7 +62,7 @@ import org.apache.solr.search.QueryUtils;
 import org.apache.solr.search.SolrConstantScoreQuery;
 import org.apache.solr.search.SyntaxError;
 
-import static org.apache.solr.parser.SolrQueryParserBase.SynonymQueryStyle.*;
+import static org.apache.solr.parser.SolrQueryParserBase.SynonymQueryStyle.AS_SAME_TERM;
 
 /** This class is overridden by QueryParser in QueryParser.jj
  * and acts to separate the majority of the Java code from the .jj grammar file.
@@ -201,6 +202,11 @@ public abstract class SolrQueryParserBase extends QueryBuilder {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
+    @Override
     public boolean equals(Object obj) {
       return false;
     }
diff --git a/solr/core/src/java/org/apache/solr/query/FilterQuery.java b/solr/core/src/java/org/apache/solr/query/FilterQuery.java
index 02cb88b..4df704e 100644
--- a/solr/core/src/java/org/apache/solr/query/FilterQuery.java
+++ b/solr/core/src/java/org/apache/solr/query/FilterQuery.java
@@ -24,6 +24,7 @@ import org.apache.lucene.search.ConstantScoreQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 import org.apache.solr.search.DocSet;
@@ -67,6 +68,10 @@ public class FilterQuery extends ExtendedQueryBase {
     return sb.toString();
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    q.visit(visitor);
+  }
 
   @Override
   public Query rewrite(IndexReader reader) throws IOException {
diff --git a/solr/core/src/java/org/apache/solr/query/SolrRangeQuery.java b/solr/core/src/java/org/apache/solr/query/SolrRangeQuery.java
index be58bbc..40a68e5 100644
--- a/solr/core/src/java/org/apache/solr/query/SolrRangeQuery.java
+++ b/solr/core/src/java/org/apache/solr/query/SolrRangeQuery.java
@@ -26,8 +26,8 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.TermStates;
 import org.apache.lucene.index.Terms;
 import org.apache.lucene.index.TermsEnum;
 import org.apache.lucene.search.BooleanClause;
@@ -40,6 +40,7 @@ import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.TermQuery;
@@ -126,6 +127,11 @@ public final class SolrRangeQuery extends ExtendedQueryBase implements DocSetPro
     return buffer.toString();
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
   private String endpoint(BytesRef ref) {
     if (ref == null) return "*";
     String toStr = Term.toString(ref);
diff --git a/solr/core/src/java/org/apache/solr/schema/LatLonType.java b/solr/core/src/java/org/apache/solr/schema/LatLonType.java
index 9f9dcd1..88ab20b 100644
--- a/solr/core/src/java/org/apache/solr/schema/LatLonType.java
+++ b/solr/core/src/java/org/apache/solr/schema/LatLonType.java
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 package org.apache.solr.schema;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -34,6 +35,7 @@ import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.SortField;
@@ -593,6 +595,11 @@ class SpatialDistanceQuery extends ExtendedQueryBase implements PostFilter {
     hash = hash * 31 + Double.doubleToLongBits(lonMin);
     return (int) (hash >> 32 + hash);
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
 
 
diff --git a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
index 0c2fb82..c875658 100644
--- a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
@@ -23,6 +23,7 @@ import java.util.Set;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Rescorer;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.TopDocsCollector;
@@ -83,4 +84,9 @@ public abstract class AbstractReRankQuery extends RankQuery {
     final Weight mainWeight = mainQuery.createWeight(searcher, scoreMode, boost);
     return new ReRankWeight(mainQuery, reRankQueryRescorer, searcher, mainWeight);
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/search/AnalyticsQuery.java b/solr/core/src/java/org/apache/solr/search/AnalyticsQuery.java
index 1928c26..b32f518 100644
--- a/solr/core/src/java/org/apache/solr/search/AnalyticsQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/AnalyticsQuery.java
@@ -17,8 +17,9 @@
 package org.apache.solr.search;
 
 import org.apache.lucene.search.IndexSearcher;
-import org.apache.solr.handler.component.ResponseBuilder;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.solr.handler.component.MergeStrategy;
+import org.apache.solr.handler.component.ResponseBuilder;
 import org.apache.solr.request.SolrRequestInfo;
 
 /**
@@ -77,4 +78,9 @@ public abstract class AnalyticsQuery extends ExtendedQueryBase implements PostFi
   }
 
   public abstract DelegatingCollector getAnalyticsCollector(ResponseBuilder rb, IndexSearcher searcher);
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
\ No newline at end of file
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 53e5acd..44787ec 100644
--- a/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/CollapsingQParserPlugin.java
@@ -54,6 +54,7 @@ import org.apache.lucene.search.FieldComparator;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LeafFieldComparator;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorable;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Sort;
@@ -263,6 +264,11 @@ public class CollapsingQParserPlugin extends QParserPlugin {
              nullPolicy == other.nullPolicy;
     }
 
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
     public int getCost() {
       return Math.max(super.getCost(), 100);
     }
diff --git a/solr/core/src/java/org/apache/solr/search/ExportQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/ExportQParserPlugin.java
index e3fbcac..bdf943e 100644
--- a/solr/core/src/java/org/apache/solr/search/ExportQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/ExportQParserPlugin.java
@@ -25,6 +25,7 @@ import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LeafCollector;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorable;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.ScoreMode;
@@ -122,6 +123,11 @@ public class ExportQParserPlugin extends QParserPlugin {
       return s;
     }
 
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
     public ExportQuery() {
 
     }
diff --git a/solr/core/src/java/org/apache/solr/search/Filter.java b/solr/core/src/java/org/apache/solr/search/Filter.java
index 4a48bb6..bd70991 100644
--- a/solr/core/src/java/org/apache/solr/search/Filter.java
+++ b/solr/core/src/java/org/apache/solr/search/Filter.java
@@ -17,16 +17,15 @@
 package org.apache.solr.search;
 
 import java.io.IOException;
-import java.util.Set;
 
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.ConstantScoreScorer;
 import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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;
@@ -93,9 +92,6 @@ public abstract class Filter extends Query {
     return new Weight(this) {
 
       @Override
-      public void extractTerms(Set<Term> terms) {}
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException {
         final Scorer scorer = scorer(context);
         final boolean match = (scorer != null && scorer.iterator().advance(doc) == doc);
@@ -142,4 +138,9 @@ public abstract class Filter extends Query {
       }
     };
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
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 0ce7f72..4ced113 100644
--- a/solr/core/src/java/org/apache/solr/search/GraphTermsQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/GraphTermsQParserPlugin.java
@@ -22,7 +22,6 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
 
 import org.apache.lucene.document.DoublePoint;
 import org.apache.lucene.document.FloatPoint;
@@ -51,6 +50,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
 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.Weight;
@@ -239,6 +239,11 @@ public class GraphTermsQParserPlugin extends QParserPlugin {
       return builder.toString();
     }
 
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
     private class WeightOrDocIdSet {
       final Weight weight;
       final DocIdSet set;
@@ -267,14 +272,6 @@ public class GraphTermsQParserPlugin extends QParserPlugin {
 
       return new ConstantScoreWeight(this, boost) {
 
-        @Override
-        public void extractTerms(Set<Term> terms) {
-          // no-op
-          // This query is for abuse cases when the number of terms is too high to
-          // run efficiently as a BooleanQuery. So likewise we hide its terms in
-          // order to protect highlighters
-        }
-
         private WeightOrDocIdSet rewrite(LeafReaderContext context) throws IOException {
           final LeafReader reader = context.reader();
           Terms terms = reader.terms(field);
@@ -781,5 +778,10 @@ abstract class PointSetQuery extends Query implements DocSetProducer {
     return sb.toString();
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
   protected abstract String toString(byte[] value);
 }
diff --git a/solr/core/src/java/org/apache/solr/search/HashQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/HashQParserPlugin.java
index 2af5bd8..2cdf9f3 100644
--- a/solr/core/src/java/org/apache/solr/search/HashQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/HashQParserPlugin.java
@@ -31,6 +31,7 @@ import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LeafCollector;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorable;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
@@ -113,6 +114,11 @@ public class HashQParserPlugin extends QParserPlugin {
              worker == other.worker;
     }
 
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
     public HashQuery(String[] keys, int workers, int worker) {
       this.keys = keys;
       this.workers = workers;
diff --git a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
index 4e832a0..c6fb0de 100644
--- a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
@@ -36,6 +36,7 @@ import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -165,6 +166,11 @@ class JoinQuery extends Query {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+
+  }
+
+  @Override
   public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
     return new JoinQueryWeight((SolrIndexSearcher) searcher, scoreMode, boost);
   }
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 b032dc0..0d37abb 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrConstantScoreQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrConstantScoreQuery.java
@@ -28,6 +28,7 @@ import org.apache.lucene.search.DocIdSet;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -138,4 +139,9 @@ public class SolrConstantScoreQuery extends Query implements ExtendedQuery {
     return 31 * classHash() + filter.hashCode();
   }
 
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
 }
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 ab0996d..210e0ad 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
@@ -2310,6 +2310,11 @@ public class SolrIndexSearcher extends IndexSearcher implements Closeable, SolrI
       return "SolrFilter";
     }
 
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
+
     private class FilterSet extends DocIdSet {
       private final DocIdSet docIdSet;
       private final LeafReaderContext context;
diff --git a/solr/core/src/java/org/apache/solr/search/WrappedQuery.java b/solr/core/src/java/org/apache/solr/search/WrappedQuery.java
index bea01a6..e123c50 100644
--- a/solr/core/src/java/org/apache/solr/search/WrappedQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/WrappedQuery.java
@@ -16,14 +16,15 @@
  */
 package org.apache.solr.search;
 
+import java.io.IOException;
+
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.Weight;
 
-import java.io.IOException;
-
 /** A simple query that wraps another query and implements ExtendedQuery. */
 public final class WrappedQuery extends ExtendedQueryBase {
   private Query q;
@@ -52,6 +53,11 @@ public final class WrappedQuery extends ExtendedQueryBase {
   }
 
   @Override
+  public void visit(QueryVisitor visitor) {
+    q.visit(visitor);
+  }
+
+  @Override
   public int hashCode() {
     return q.hashCode();
   }
diff --git a/solr/core/src/java/org/apache/solr/search/join/BlockJoinFacetFilter.java b/solr/core/src/java/org/apache/solr/search/join/BlockJoinFacetFilter.java
index 83e9a8f..5d65422 100644
--- a/solr/core/src/java/org/apache/solr/search/join/BlockJoinFacetFilter.java
+++ b/solr/core/src/java/org/apache/solr/search/join/BlockJoinFacetFilter.java
@@ -20,6 +20,7 @@ import java.util.Objects;
 
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.solr.search.DelegatingCollector;
 import org.apache.solr.search.PostFilter;
 
@@ -87,4 +88,9 @@ class BlockJoinFacetFilter extends Query implements PostFilter {
   public int hashCode() {
     return classHash() * 31 + blockJoinFacetCollector.hashCode();
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/search/join/GraphQuery.java b/solr/core/src/java/org/apache/solr/search/join/GraphQuery.java
index 4402a26..accb1dc 100644
--- a/solr/core/src/java/org/apache/solr/search/join/GraphQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/join/GraphQuery.java
@@ -20,7 +20,6 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import java.util.TreeSet;
 
 import org.apache.lucene.index.LeafReaderContext;
@@ -34,6 +33,7 @@ import org.apache.lucene.search.DocValuesFieldExistsQuery;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -281,11 +281,6 @@ public class GraphQuery extends Query {
     public boolean isCacheable(LeafReaderContext ctx) {
       return true;
     }
-
-    @Override
-    public void extractTerms(Set<Term> terms) {
-      // NoOp for now , not used.. / supported
-    }
     
   }
   
@@ -441,5 +436,10 @@ public class GraphQuery extends Query {
            Objects.equals(toField, other.toField) &&
            Objects.equals(traversalFilter, other.traversalFilter);
   }
-  
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
+
 }
diff --git a/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
index 55d58fc..c2f8662 100644
--- a/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
@@ -23,6 +23,7 @@ import java.util.Objects;
 import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Weight;
 import org.apache.lucene.search.join.JoinUtil;
 import org.apache.lucene.search.join.ScoreMode;
@@ -195,6 +196,11 @@ public class ScoreJoinQParserPlugin extends QParserPlugin {
              Objects.equals(scoreMode, other.scoreMode) &&
              Objects.equals(toField, other.toField);
     }
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
   }
 
 
diff --git a/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java b/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java
index 290b7c1..fe315d2 100644
--- a/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java
+++ b/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java
@@ -27,10 +27,10 @@ import java.util.Map.Entry;
 import java.util.Set;
 
 import com.google.common.collect.Lists;
-import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
 import org.apache.lucene.search.CollectionStatistics;
+import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.ScoreMode;
 import org.apache.lucene.search.TermStatistics;
@@ -157,26 +157,32 @@ public class ExactStatsCache extends StatsCache {
     Query q = rb.getQuery();
     try {
       HashSet<Term> terms = new HashSet<>();
-      searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1).extractTerms(terms);
-      IndexReaderContext context = searcher.getTopReaderContext();
       HashMap<String,TermStats> statsMap = new HashMap<>();
       HashMap<String,CollectionStats> colMap = new HashMap<>();
-      for (Term t : terms) {
-        TermStates termStates = TermStates.build(context, t, true);
-
-        if (!colMap.containsKey(t.field())) { // collection stats for this field
-          CollectionStatistics collectionStatistics = searcher.localCollectionStatistics(t.field());
-          if (collectionStatistics != null) {
-            colMap.put(t.field(), new CollectionStats(collectionStatistics));
+      IndexSearcher statsCollectingSearcher = new IndexSearcher(searcher.getIndexReader()){
+        @Override
+        public CollectionStatistics collectionStatistics(String field) throws IOException {
+          CollectionStatistics cs = super.collectionStatistics(field);
+          if (cs != null) {
+            colMap.put(field, new CollectionStats(cs));
           }
+          return cs;
         }
 
-        TermStatistics tst = searcher.localTermStatistics(t, termStates);
-        if (tst == null) { // skip terms that are not present here
-          continue;
+        @Override
+        public TermStatistics termStatistics(Term term, TermStates context) throws IOException {
+          TermStatistics ts = super.termStatistics(term, context);
+          if (ts == null) {
+            return null;
+          }
+          terms.add(term);
+          statsMap.put(term.toString(), new TermStats(term.field(), ts));
+          return ts;
         }
+      };
+      statsCollectingSearcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);
 
-        statsMap.put(t.toString(), new TermStats(t.field(), tst));
+      for (Term t : terms) {
         rb.rsp.add(TERMS_KEY, t.toString());
       }
       if (statsMap.size() != 0) { //Don't add empty keys
diff --git a/solr/core/src/java/org/apache/solr/update/DeleteByQueryWrapper.java b/solr/core/src/java/org/apache/solr/update/DeleteByQueryWrapper.java
index 9a16f60..08c1540 100644
--- a/solr/core/src/java/org/apache/solr/update/DeleteByQueryWrapper.java
+++ b/solr/core/src/java/org/apache/solr/update/DeleteByQueryWrapper.java
@@ -18,15 +18,14 @@ package org.apache.solr.update;
 
 import java.io.IOException;
 import java.util.Objects;
-import java.util.Set;
 
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.Term;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -73,11 +72,6 @@ final class DeleteByQueryWrapper extends Query {
     final Weight inner = in.createWeight(privateContext, scoreMode, boost);
     return new Weight(DeleteByQueryWrapper.this) {
       @Override
-      public void extractTerms(Set<Term> terms) {
-        throw new UnsupportedOperationException();
-      }
-
-      @Override
       public Explanation explain(LeafReaderContext context, int doc) throws IOException { throw new UnsupportedOperationException(); }
 
       @Override
@@ -116,4 +110,9 @@ final class DeleteByQueryWrapper extends Query {
     return Objects.equals(in, other.in) &&
            Objects.equals(schema, other.schema);
   }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    visitor.visitLeaf(this);
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java b/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
index 0360afc..b56bcb6 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
@@ -45,6 +45,7 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.IndexSearcher;
 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.Weight;
@@ -529,6 +530,11 @@ public class SolrIndexSplitter {
     public int hashCode() {
       return partition;
     }
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+      visitor.visitLeaf(this);
+    }
   }
 
   static FixedBitSet[] split(LeafReaderContext readerContext, int numPieces, SchemaField field, DocRouter.Range[] rangesArr,
diff --git a/solr/core/src/test/org/apache/solr/search/RankQueryTestPlugin.java b/solr/core/src/test/org/apache/solr/search/RankQueryTestPlugin.java
index bc38397..1024883 100644
--- a/solr/core/src/test/org/apache/solr/search/RankQueryTestPlugin.java
+++ b/solr/core/src/test/org/apache/solr/search/RankQueryTestPlugin.java
@@ -36,6 +36,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.LeafCollector;
 import org.apache.lucene.search.LeafFieldComparator;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
 import org.apache.lucene.search.Scorable;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.ScoreMode;
@@ -112,6 +113,11 @@ public class RankQueryTestPlugin extends QParserPlugin {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return q.toString(field);
     }
diff --git a/solr/core/src/test/org/apache/solr/uninverting/TestFieldCacheSortRandom.java b/solr/core/src/test/org/apache/solr/uninverting/TestFieldCacheSortRandom.java
index bca4794..9152632 100644
--- a/solr/core/src/test/org/apache/solr/uninverting/TestFieldCacheSortRandom.java
+++ b/solr/core/src/test/org/apache/solr/uninverting/TestFieldCacheSortRandom.java
@@ -44,6 +44,7 @@ import org.apache.lucene.search.ConstantScoreWeight;
 import org.apache.lucene.search.FieldDoc;
 import org.apache.lucene.search.IndexSearcher;
 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.Sort;
@@ -54,8 +55,8 @@ import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.BitSetIterator;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.FixedBitSet;
-import org.apache.solr.SolrTestCase;
 import org.apache.lucene.util.TestUtil;
+import org.apache.solr.SolrTestCase;
 import org.apache.solr.uninverting.UninvertingReader.Type;
 
 /** random sorting tests with uninversion */
@@ -298,6 +299,11 @@ public class TestFieldCacheSortRandom extends SolrTestCase {
     }
 
     @Override
+    public void visit(QueryVisitor visitor) {
+
+    }
+
+    @Override
     public String toString(String field) {
       return "RandomFilter(density=" + density + ")";
     }