diff --git a/lucene/core/src/java/org/apache/lucene/search/DisiWrapper.java b/lucene/core/src/java/org/apache/lucene/search/DisiWrapper.java
index fac9418010f..4868b5c4e74 100644
--- a/lucene/core/src/java/org/apache/lucene/search/DisiWrapper.java
+++ b/lucene/core/src/java/org/apache/lucene/search/DisiWrapper.java
@@ -40,6 +40,8 @@ public class DisiWrapper {
 
   // For WANDScorer
   long maxScore;
+  int twoPhaseDoc = -1;
+  boolean twoPhaseMatch;
 
   // FOR SPANS
   public final Spans spans;
diff --git a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
index a633f0859a0..8760c969f8a 100644
--- a/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java
@@ -130,6 +130,8 @@ final class WANDScorer extends Scorer {
 
   int upTo; // upper bound for which max scores are valid
 
+  final boolean hasTwoPhaseView;
+
   WANDScorer(Weight weight, Collection<Scorer> scorers) throws IOException {
     super(weight);
 
@@ -154,12 +156,15 @@ final class WANDScorer extends Scorer {
     this.scalingFactor = scalingFactor.orElse(0);
 
     long cost = 0;
+    boolean hasTwoPhase = false;
     for (Scorer scorer : scorers) {
       DisiWrapper w = new DisiWrapper(scorer);
       cost += w.cost;
+      hasTwoPhase |= w.twoPhaseView != null;
       addLead(w);
     }
     this.cost = cost;
+    this.hasTwoPhaseView = hasTwoPhase;
     this.maxScorePropagator = new MaxScoreSumPropagator(scorers);
   }
 
@@ -257,20 +262,54 @@ final class WANDScorer extends Scorer {
         return cost;
       }
     };
-    return new TwoPhaseIterator(approximation) {
 
+    return new TwoPhaseIterator(approximation) {
       @Override
       public boolean matches() throws IOException {
+
         while (leadMaxScore < minCompetitiveScore) {
           if (leadMaxScore + tailMaxScore >= minCompetitiveScore) {
             // a match on doc is still possible, try to
             // advance scorers from the tail
-            advanceTail();
+            DisiWrapper w = advanceTail();
           } else {
             return false;
           }
         }
-        return true;
+
+        if (hasTwoPhaseView == false) {
+          return true;
+        }
+
+        // check if one of the clause in head matches (approximation + twoPhaseView)
+        long matchMaxScore = leadMaxScore + tailMaxScore;
+        for (DisiWrapper s = lead; s != null; s = s.next) {
+          if (getOrCacheTwoPhase(s)) {
+            return true;
+          }
+          // the clause doesn't match so we can check if the document
+          // is still competitive if we remove its max score
+          matchMaxScore -= s.maxScore;
+          if (matchMaxScore < minCompetitiveScore) {
+            return false;
+          }
+        }
+
+        // check if one of the clause in tail matches (approximation + twoPhaseView)
+        while (tailSize > 0) {
+          DisiWrapper s = advanceTail();
+          if (s.approximation.docID() == doc
+                && getOrCacheTwoPhase(s)) {
+            return true;
+          }
+          // the clause doesn't match so we can check if the document
+          // is still competitive if we remove its max score.
+          matchMaxScore -= s.maxScore;
+          if (matchMaxScore < minCompetitiveScore) {
+            return false;
+          }
+        }
+        return false;
       }
 
       @Override
@@ -282,6 +321,21 @@ final class WANDScorer extends Scorer {
     };
   }
 
+  /**
+   * Checks if the provided {@link DisiWrapper} matches the current document
+   * and cache the result in the wrapper if not not done already.
+   **/
+  private boolean getOrCacheTwoPhase(DisiWrapper w) throws IOException {
+    if (w.twoPhaseView == null) {
+      return true;
+    } else if (w.twoPhaseDoc == doc) {
+      return w.twoPhaseMatch;
+    } else {
+      w.twoPhaseDoc = w.doc;
+      return w.twoPhaseMatch = w.twoPhaseView.matches();
+    }
+  }
+
   /** Add a disi to the linked list of leads. */
   private void addLead(DisiWrapper lead) {
     lead.next = this.lead;
@@ -294,7 +348,7 @@ final class WANDScorer extends Scorer {
     for (DisiWrapper s = lead; s != null; s = s.next) {
       final DisiWrapper evicted = insertTailWithOverFlow(s);
       if (evicted != null) {
-        evicted.doc = evicted.iterator.advance(target);
+        evicted.doc = evicted.approximation.advance(target);
         head.add(evicted);
       }
     }
@@ -308,7 +362,7 @@ final class WANDScorer extends Scorer {
     while (headTop != null && headTop.doc < target) {
       final DisiWrapper evicted = insertTailWithOverFlow(headTop);
       if (evicted != null) {
-        evicted.doc = evicted.iterator.advance(target);
+        evicted.doc = evicted.approximation.advance(target);
         headTop = head.updateTop(evicted);
       } else {
         head.pop();
@@ -318,7 +372,7 @@ final class WANDScorer extends Scorer {
   }
 
   private void advanceTail(DisiWrapper disi) throws IOException {
-    disi.doc = disi.iterator.advance(doc);
+    disi.doc = disi.approximation.advance(doc);
     if (disi.doc == doc) {
       addLead(disi);
     } else {
@@ -329,9 +383,10 @@ final class WANDScorer extends Scorer {
   /** Pop the entry from the 'tail' that has the greatest score contribution,
    *  advance it to the current doc and then add it to 'lead' or 'head'
    *  depending on whether it matches. */
-  private void advanceTail() throws IOException {
+  private DisiWrapper advanceTail() throws IOException {
     final DisiWrapper top = popTail();
     advanceTail(top);
+    return top;
   }
 
   private void updateMaxScores(int target) throws IOException {
@@ -369,7 +424,7 @@ final class WANDScorer extends Scorer {
     // a competitive hit.
     while (tailSize > 0 && tailMaxScore >= minCompetitiveScore) {
       DisiWrapper w = popTail();
-      w.doc = w.iterator.advance(target);
+      w.doc = w.approximation.advance(target);
       head.add(w);
     }
   }
@@ -452,7 +507,9 @@ final class WANDScorer extends Scorer {
     advanceAllTail();
     double score = 0;
     for (DisiWrapper s = lead; s != null; s = s.next) {
-      score += s.scorer.score();
+      if (getOrCacheTwoPhase(s)) {
+        score += s.scorer.score();
+      }
     }
     return (float) score;
   }
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 40caaecf0fc..3c1b44bdd9e 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java
@@ -19,13 +19,18 @@ package org.apache.lucene.search;
 import java.io.IOException;
 import java.util.Arrays;
 
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field.Store;
 import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.MergeScheduler;
+import org.apache.lucene.index.SerialMergeScheduler;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.store.Directory;
@@ -50,8 +55,8 @@ public class TestWANDScorer extends LuceneTestCase {
   private void doTestScalingFactor(float f) {
     int scalingFactor = WANDScorer.scalingFactor(f);
     float scaled = Math.scalb(f, scalingFactor);
-    assertTrue(""+scaled, scaled > 1 << 15);
-    assertTrue(""+scaled, scaled <= 1 << 16);
+    assertTrue("" + scaled, scaled > 1 << 15);
+    assertTrue("" + scaled, scaled <= 1 << 16);
   }
 
   private Query maybeWrap(Query query) {
@@ -62,17 +67,25 @@ public class TestWANDScorer extends LuceneTestCase {
     return query;
   }
 
+  private Query maybeWrapTwoPhase(Query query) {
+    if (random().nextBoolean()) {
+      query = new RandomApproximationQuery(query, random());
+      query = new AssertingQuery(random(), query);
+    }
+    return query;
+  }
+
   public void testBasics() throws Exception {
     Directory dir = newDirectory();
     IndexWriter w = new IndexWriter(dir, newIndexWriterConfig().setMergePolicy(newLogMergePolicy()));
     for (String[] values : Arrays.asList(
-        new String[]{ "A", "B" },       // 0
-        new String[]{ "A" },            // 1
-        new String[]{ },                // 2
-        new String[]{ "A", "B", "C" },  // 3
-        new String[]{ "B" },            // 4
-        new String[]{ "B", "C" }        // 5
-        )) {
+        new String[]{"A", "B"},       // 0
+        new String[]{"A"},            // 1
+        new String[]{},                // 2
+        new String[]{"A", "B", "C"},  // 3
+        new String[]{"B"},            // 4
+        new String[]{"B", "C"}        // 5
+    )) {
       Document doc = new Document();
       for (String value : values) {
         doc.add(new StringField("foo", value, Store.NO));
@@ -140,9 +153,9 @@ public class TestWANDScorer extends LuceneTestCase {
     query = new BooleanQuery.Builder()
         .add(
             new BooleanQuery.Builder()
-            .add(new BoostQuery(new ConstantScoreQuery(new TermQuery(new Term("foo", "A"))), 2), Occur.SHOULD)
-            .add(new ConstantScoreQuery(new TermQuery(new Term("foo", "B"))), Occur.SHOULD)
-            .build(), Occur.MUST)
+                .add(new BoostQuery(new ConstantScoreQuery(new TermQuery(new Term("foo", "A"))), 2), Occur.SHOULD)
+                .add(new ConstantScoreQuery(new TermQuery(new Term("foo", "B"))), Occur.SHOULD)
+                .build(), Occur.MUST)
         .add(new TermQuery(new Term("foo", "C")), Occur.FILTER)
         .build();
 
@@ -206,6 +219,165 @@ public class TestWANDScorer extends LuceneTestCase {
     dir.close();
   }
 
+  public void testBasicsTwoPhase() throws Exception {
+    Directory dir = newDirectory();
+    MockAnalyzer a = new MockAnalyzer(random());
+    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig(a)
+        .setMergePolicy(newLogMergePolicy())
+    );
+    for (String[] values : Arrays.asList(
+        new String[]{"A B", "B C", "C D"},         // 0
+        new String[]{"A B", "E J", "A D"},         // 1
+        new String[]{"A A", "C C", "B B", "D D"},  // 2
+        new String[]{"A W", "E F", "B C"},           // 3
+        new String[]{"B C", "A B", "E F"},         // 4
+        new String[]{"B B", "B C", "C D"}          // 5
+    )) {
+      Document doc = new Document();
+      for (String value : values) {
+        doc.add(new TextField("foo", value, Store.NO));
+      }
+      w.addDocument(doc);
+    }
+    w.forceMerge(1);
+    w.close();
+
+    IndexReader reader = DirectoryReader.open(dir);
+    IndexSearcher searcher = newSearcher(reader);
+    PhraseQuery pq1 = new PhraseQuery.Builder()
+        .add(new Term("foo", "a"))
+        .add(new Term("foo", "b"))
+        .build();
+    PhraseQuery pq2 = new PhraseQuery.Builder()
+        .add(new Term("foo", "b"))
+        .add(new Term("foo", "c"))
+        .build();
+    PhraseQuery pq3 = new PhraseQuery.Builder()
+        .add(new Term("foo", "c"))
+        .add(new Term("foo", "d"))
+        .build();
+
+    Query query = new BooleanQuery.Builder()
+        .add(new BoostQuery(new ConstantScoreQuery(pq1), 2), Occur.SHOULD)
+        .add(new ConstantScoreQuery(pq2), Occur.SHOULD)
+        .add(new BoostQuery(new ConstantScoreQuery(pq3), 3), Occur.SHOULD)
+        .build();
+
+    Scorer scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+
+    assertEquals(0, scorer.iterator().nextDoc());
+    assertEquals(3 + 2 + 1, scorer.score(), 0);
+
+    assertEquals(1, scorer.iterator().nextDoc());
+    assertEquals(2, scorer.score(), 0);
+
+    assertEquals(3, scorer.iterator().nextDoc());
+    assertEquals(1, scorer.score(), 0);
+
+    assertEquals(4, scorer.iterator().nextDoc());
+    assertEquals(2 + 1, scorer.score(), 0);
+
+    assertEquals(5, scorer.iterator().nextDoc());
+    assertEquals(1 + 3, scorer.score(), 0);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+    scorer.setMinCompetitiveScore(4);
+
+    assertEquals(0, scorer.iterator().nextDoc());
+    assertEquals(3 + 2 + 1, scorer.score(), 0);
+
+    assertEquals(5, scorer.iterator().nextDoc());
+    assertEquals(1 + 3, scorer.score(), 0);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+
+    assertEquals(0, scorer.iterator().nextDoc());
+    assertEquals(3 + 2 + 1, scorer.score(), 0);
+
+    scorer.setMinCompetitiveScore(10);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    //  test a filtered disjunction
+    query = new BooleanQuery.Builder()
+        .add(
+            new BooleanQuery.Builder()
+                .add(new BoostQuery(new ConstantScoreQuery(pq1), 2), Occur.SHOULD)
+                .add(new ConstantScoreQuery(pq2), Occur.SHOULD)
+                .build(), Occur.MUST)
+        .add(pq3, Occur.FILTER)
+        .build();
+
+    scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+
+    assertEquals(0, scorer.iterator().nextDoc());
+    assertEquals(2 + 1, scorer.score(), 0);
+
+    assertEquals(5, scorer.iterator().nextDoc());
+    assertEquals(1, scorer.score(), 0);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+
+    scorer.setMinCompetitiveScore(2);
+
+    assertEquals(0, scorer.iterator().nextDoc());
+    assertEquals(2 + 1, scorer.score(), 0);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    // Now test a filtered disjunction with a MUST_NOT
+    query = new BooleanQuery.Builder()
+        .add(new BoostQuery(new ConstantScoreQuery(pq1), 2), Occur.SHOULD)
+        .add(new ConstantScoreQuery(pq2), Occur.SHOULD)
+        .add(pq3, Occur.MUST_NOT)
+        .build();
+
+    scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+
+    assertEquals(1, scorer.iterator().nextDoc());
+    assertEquals(2, scorer.score(), 0);
+
+    assertEquals(3, scorer.iterator().nextDoc());
+    assertEquals(1, scorer.score(), 0);
+
+    assertEquals(4, scorer.iterator().nextDoc());
+    assertEquals(2 + 1, scorer.score(), 0);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    scorer = searcher
+        .createWeight(searcher.rewrite(query), ScoreMode.TOP_SCORES, 1)
+        .scorer(searcher.getIndexReader().leaves().get(0));
+
+    scorer.setMinCompetitiveScore(3);
+
+    assertEquals(4, scorer.iterator().nextDoc());
+    assertEquals(2 + 1, scorer.score(), 0);
+
+    assertEquals(DocIdSetIterator.NO_MORE_DOCS, scorer.iterator().nextDoc());
+
+    reader.close();
+    dir.close();
+  }
+
   public void testRandom() throws IOException {
     Directory dir = newDirectory();
     IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
@@ -246,6 +418,39 @@ public class TestWANDScorer extends LuceneTestCase {
     dir.close();
   }
 
+  public void testRandomTwoPhase() throws IOException {
+    Directory dir = newDirectory();
+    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
+    int numDocs = atLeast(1000);
+    for (int i = 0; i < numDocs; ++i) {
+      Document doc = new Document();
+      int numValues = random().nextInt(1 << random().nextInt(5));
+      int start = random().nextInt(10);
+      for (int j = 0; j < numValues; ++j) {
+        doc.add(new StringField("foo", Integer.toString(start + j), Store.NO));
+      }
+      w.addDocument(doc);
+    }
+    IndexReader reader = DirectoryReader.open(w);
+    w.close();
+    IndexSearcher searcher = newSearcher(reader);
+
+    for (int iter = 0; iter < 100; ++iter) {
+      int start = random().nextInt(10);
+      int numClauses = random().nextInt(1 << random().nextInt(5));
+      BooleanQuery.Builder builder = new BooleanQuery.Builder();
+      for (int i = 0; i < numClauses; ++i) {
+        builder.add(maybeWrapTwoPhase(new TermQuery(new Term("foo", Integer.toString(start + i)))), Occur.SHOULD);
+      }
+      int filterTerm = random().nextInt(30);
+      builder.add(new TermQuery(new Term("foo", Integer.toString(filterTerm))), Occur.FILTER);
+      Query query = builder.build();
+      CheckHits.checkTopScores(random(), query, searcher);
+    }
+    reader.close();
+    dir.close();
+  }
+
   public void testRandomWithInfiniteMaxScore() throws IOException {
     Directory dir = newDirectory();
     IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
