You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by jp...@apache.org on 2019/07/09 14:01:35 UTC

[lucene-solr] branch master updated: LUCENE-8311: Phrase impacts (#760)

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

jpountz 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 cfac486  LUCENE-8311: Phrase impacts (#760)
cfac486 is described below

commit cfac486afd7bce64c10497a3b9e541d64ee4f1fd
Author: Adrien Grand <jp...@gmail.com>
AuthorDate: Tue Jul 9 16:01:29 2019 +0200

    LUCENE-8311: Phrase impacts (#760)
---
 .../codecs/lucene50/Lucene50PostingsReader.java    | 528 ++++++++++-----------
 .../apache/lucene/search/ExactPhraseMatcher.java   | 209 +++++++-
 .../org/apache/lucene/search/MultiPhraseQuery.java |  10 +-
 .../org/apache/lucene/search/PhraseMatcher.java    |  15 +-
 .../java/org/apache/lucene/search/PhraseQuery.java |  28 +-
 .../org/apache/lucene/search/PhraseScorer.java     |  18 +-
 .../org/apache/lucene/search/PhraseWeight.java     |  12 +-
 .../apache/lucene/search/SloppyPhraseMatcher.java  |  60 ++-
 .../org/apache/lucene/search/TestPhraseQuery.java  | 312 +++++++++++-
 .../queries/intervals/TermIntervalsSource.java     |   4 +-
 10 files changed, 867 insertions(+), 329 deletions(-)

diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50PostingsReader.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50PostingsReader.java
index d009d9d..0ea8c80 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50PostingsReader.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene50/Lucene50PostingsReader.java
@@ -197,8 +197,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
   public PostingsEnum postings(FieldInfo fieldInfo, BlockTermState termState, PostingsEnum reuse, int flags) throws IOException {
     
     boolean indexHasPositions = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) >= 0;
-    boolean indexHasOffsets = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0;
-    boolean indexHasPayloads = fieldInfo.hasPayloads();
 
     if (indexHasPositions == false || PostingsEnum.featureRequested(flags, PostingsEnum.POSITIONS) == false) {
       BlockDocsEnum docsEnum;
@@ -211,18 +209,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         docsEnum = new BlockDocsEnum(fieldInfo);
       }
       return docsEnum.reset((IntBlockTermState) termState, flags);
-    } else if ((indexHasOffsets == false || PostingsEnum.featureRequested(flags, PostingsEnum.OFFSETS) == false) &&
-               (indexHasPayloads == false || PostingsEnum.featureRequested(flags, PostingsEnum.PAYLOADS) == false)) {
-      BlockPostingsEnum docsAndPositionsEnum;
-      if (reuse instanceof BlockPostingsEnum) {
-        docsAndPositionsEnum = (BlockPostingsEnum) reuse;
-        if (!docsAndPositionsEnum.canReuse(docIn, fieldInfo)) {
-          docsAndPositionsEnum = new BlockPostingsEnum(fieldInfo);
-        }
-      } else {
-        docsAndPositionsEnum = new BlockPostingsEnum(fieldInfo);
-      }
-      return docsAndPositionsEnum.reset((IntBlockTermState) termState);
     } else {
       EverythingEnum everythingEnum;
       if (reuse instanceof EverythingEnum) {
@@ -243,6 +229,18 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
       // no skip data
       return new SlowImpactsEnum(postings(fieldInfo, state, null, flags));
     }
+
+    final boolean indexHasPositions = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) >= 0;
+    final boolean indexHasOffsets = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0;
+    final boolean indexHasPayloads = fieldInfo.hasPayloads();
+
+    if (indexHasPositions &&
+        PostingsEnum.featureRequested(flags, PostingsEnum.POSITIONS) &&
+        (indexHasOffsets == false || PostingsEnum.featureRequested(flags, PostingsEnum.OFFSETS) == false) &&
+        (indexHasPayloads == false || PostingsEnum.featureRequested(flags, PostingsEnum.PAYLOADS) == false)) {
+      return new BlockImpactsPostingsEnum(fieldInfo, (IntBlockTermState) state);
+    }
+
     return new BlockImpactsEverythingEnum(fieldInfo, (IntBlockTermState) state, flags);
   }
 
@@ -493,8 +491,8 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     }
   }
 
-
-  final class BlockPostingsEnum extends PostingsEnum {
+  // Also handles payloads + offsets
+  final class EverythingEnum extends PostingsEnum {
 
     private final byte[] encoded;
 
@@ -502,6 +500,18 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     private final int[] freqBuffer = new int[MAX_DATA_SIZE];
     private final int[] posDeltaBuffer = new int[MAX_DATA_SIZE];
 
+    private final int[] payloadLengthBuffer;
+    private final int[] offsetStartDeltaBuffer;
+    private final int[] offsetLengthBuffer;
+
+    private byte[] payloadBytes;
+    private int payloadByteUpto;
+    private int payloadLength;
+
+    private int lastStartOffset;
+    private int startOffset;
+    private int endOffset;
+
     private int docBufferUpto;
     private int posBufferUpto;
 
@@ -512,6 +522,8 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
 
     IndexInput docIn;
     final IndexInput posIn;
+    final IndexInput payIn;
+    final BytesRef payload;
 
     final boolean indexHasOffsets;
     final boolean indexHasPayloads;
@@ -532,6 +544,10 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     // before reading positions:
     private long posPendingFP;
 
+    // Lazy pay seek: if != -1 then we must seek to this FP
+    // before reading payloads/offsets:
+    private long payPendingFP;
+
     // Where this term's postings start in the .doc file:
     private long docTermStartFP;
 
@@ -554,15 +570,42 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
 
     private int nextSkipDoc;
 
+    private boolean needsOffsets; // true if we actually need offsets
+    private boolean needsPayloads; // true if we actually need payloads
     private int singletonDocID; // docid when there is a single pulsed posting, otherwise -1
 
-    public BlockPostingsEnum(FieldInfo fieldInfo) throws IOException {
+    public EverythingEnum(FieldInfo fieldInfo) throws IOException {
+      indexHasOffsets = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0;
+      indexHasPayloads = fieldInfo.hasPayloads();
+
       this.startDocIn = Lucene50PostingsReader.this.docIn;
       this.docIn = null;
       this.posIn = Lucene50PostingsReader.this.posIn.clone();
+      if (indexHasOffsets || indexHasPayloads) {
+        this.payIn = Lucene50PostingsReader.this.payIn.clone();
+      } else {
+        this.payIn = null;
+      }
       encoded = new byte[MAX_ENCODED_SIZE];
-      indexHasOffsets = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0;
-      indexHasPayloads = fieldInfo.hasPayloads();
+      if (indexHasOffsets) {
+        offsetStartDeltaBuffer = new int[MAX_DATA_SIZE];
+        offsetLengthBuffer = new int[MAX_DATA_SIZE];
+      } else {
+        offsetStartDeltaBuffer = null;
+        offsetLengthBuffer = null;
+        startOffset = -1;
+        endOffset = -1;
+      }
+
+      if (indexHasPayloads) {
+        payloadLengthBuffer = new int[MAX_DATA_SIZE];
+        payloadBytes = new byte[128];
+        payload = new BytesRef();
+      } else {
+        payloadLengthBuffer = null;
+        payloadBytes = null;
+        payload = null;
+      }
     }
 
     public boolean canReuse(IndexInput docIn, FieldInfo fieldInfo) {
@@ -571,7 +614,7 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         indexHasPayloads == fieldInfo.hasPayloads();
     }
 
-    public PostingsEnum reset(IntBlockTermState termState) throws IOException {
+    public EverythingEnum reset(IntBlockTermState termState, int flags) throws IOException {
       docFreq = termState.docFreq;
       docTermStartFP = termState.docStartFP;
       posTermStartFP = termState.posStartFP;
@@ -587,6 +630,7 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         docIn.seek(docTermStartFP);
       }
       posPendingFP = posTermStartFP;
+      payPendingFP = payTermStartFP;
       posPendingCount = 0;
       if (termState.totalTermFreq < BLOCK_SIZE) {
         lastPosBlockFP = posTermStartFP;
@@ -596,6 +640,9 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         lastPosBlockFP = posTermStartFP + termState.lastPosBlockOffset;
       }
 
+      this.needsOffsets = PostingsEnum.featureRequested(flags, PostingsEnum.OFFSETS);
+      this.needsPayloads = PostingsEnum.featureRequested(flags, PostingsEnum.PAYLOADS);
+
       doc = -1;
       accum = 0;
       docUpto = 0;
@@ -630,7 +677,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         docDeltaBuffer[0] = singletonDocID;
         freqBuffer[0] = (int) totalTermFreq;
       } else {
-        // Read vInts:
         readVIntBlock(docIn, docDeltaBuffer, freqBuffer, left, true);
       }
       docBufferUpto = 0;
@@ -640,28 +686,68 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
       if (posIn.getFilePointer() == lastPosBlockFP) {
         final int count = (int) (totalTermFreq % BLOCK_SIZE);
         int payloadLength = 0;
+        int offsetLength = 0;
+        payloadByteUpto = 0;
         for(int i=0;i<count;i++) {
           int code = posIn.readVInt();
           if (indexHasPayloads) {
             if ((code & 1) != 0) {
               payloadLength = posIn.readVInt();
             }
+            payloadLengthBuffer[i] = payloadLength;
             posDeltaBuffer[i] = code >>> 1;
             if (payloadLength != 0) {
-              posIn.seek(posIn.getFilePointer() + payloadLength);
+              if (payloadByteUpto + payloadLength > payloadBytes.length) {
+                payloadBytes = ArrayUtil.grow(payloadBytes, payloadByteUpto + payloadLength);
+              }
+              posIn.readBytes(payloadBytes, payloadByteUpto, payloadLength);
+              payloadByteUpto += payloadLength;
             }
           } else {
             posDeltaBuffer[i] = code;
           }
+
           if (indexHasOffsets) {
-            if ((posIn.readVInt() & 1) != 0) {
-              // offset length changed
-              posIn.readVInt();
+            int deltaCode = posIn.readVInt();
+            if ((deltaCode & 1) != 0) {
+              offsetLength = posIn.readVInt();
             }
+            offsetStartDeltaBuffer[i] = deltaCode >>> 1;
+            offsetLengthBuffer[i] = offsetLength;
           }
         }
+        payloadByteUpto = 0;
       } else {
         forUtil.readBlock(posIn, encoded, posDeltaBuffer);
+
+        if (indexHasPayloads) {
+          if (needsPayloads) {
+            forUtil.readBlock(payIn, encoded, payloadLengthBuffer);
+            int numBytes = payIn.readVInt();
+
+            if (numBytes > payloadBytes.length) {
+              payloadBytes = ArrayUtil.grow(payloadBytes, numBytes);
+            }
+            payIn.readBytes(payloadBytes, 0, numBytes);
+          } else {
+            // this works, because when writing a vint block we always force the first length to be written
+            forUtil.skipBlock(payIn); // skip over lengths
+            int numBytes = payIn.readVInt(); // read length of payloadBytes
+            payIn.seek(payIn.getFilePointer() + numBytes); // skip over payloadBytes
+          }
+          payloadByteUpto = 0;
+        }
+
+        if (indexHasOffsets) {
+          if (needsOffsets) {
+            forUtil.readBlock(payIn, encoded, offsetStartDeltaBuffer);
+            forUtil.readBlock(payIn, encoded, offsetLengthBuffer);
+          } else {
+            // this works, because when writing a vint block we always force the first length to be written
+            forUtil.skipBlock(payIn); // skip over starts
+            forUtil.skipBlock(payIn); // skip over lengths
+          }
+        }
       }
     }
 
@@ -682,6 +768,7 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
 
       doc = accum;
       position = 0;
+      lastStartOffset = 0;
       return doc;
     }
 
@@ -693,11 +780,11 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         if (skipper == null) {
           // Lazy init: first time this enum has ever been used for skipping
           skipper = new Lucene50SkipReader(version,
-                                           docIn.clone(),
-                                           MAX_SKIP_LEVELS,
-                                           true,
-                                           indexHasOffsets,
-                                           indexHasPayloads);
+                                        docIn.clone(),
+                                        MAX_SKIP_LEVELS,
+                                        true,
+                                        indexHasOffsets,
+                                        indexHasPayloads);
         }
 
         if (!skipped) {
@@ -712,7 +799,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
 
         if (newDocUpto > docUpto) {
           // Skipper moved
-
           assert newDocUpto % BLOCK_SIZE == 0 : "got " + newDocUpto;
           docUpto = newDocUpto;
 
@@ -721,7 +807,10 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
           accum = skipper.getDoc();
           docIn.seek(skipper.getDocPointer());
           posPendingFP = skipper.getPosPointer();
+          payPendingFP = skipper.getPayPointer();
           posPendingCount = skipper.getPosBufferUpto();
+          lastStartOffset = 0; // new document
+          payloadByteUpto = skipper.getPayloadByteUpto();
         }
         nextSkipDoc = skipper.getNextSkipDoc();
       }
@@ -732,8 +821,7 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         refillDocs();
       }
 
-      // Now scan... this is an inlined/pared down version
-      // of nextDoc():
+      // Now scan:
       while (true) {
         accum += docDeltaBuffer[docBufferUpto];
         freq = freqBuffer[docBufferUpto];
@@ -750,6 +838,7 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
       }
 
       position = 0;
+      lastStartOffset = 0;
       return doc = accum;
     }
 
@@ -760,33 +849,68 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     private void skipPositions() throws IOException {
       // Skip positions now:
       int toSkip = posPendingCount - freq;
+      // if (DEBUG) {
+      //   System.out.println("      FPR.skipPositions: toSkip=" + toSkip);
+      // }
 
       final int leftInBlock = BLOCK_SIZE - posBufferUpto;
       if (toSkip < leftInBlock) {
-        posBufferUpto += toSkip;
+        int end = posBufferUpto + toSkip;
+        while(posBufferUpto < end) {
+          if (indexHasPayloads) {
+            payloadByteUpto += payloadLengthBuffer[posBufferUpto];
+          }
+          posBufferUpto++;
+        }
       } else {
         toSkip -= leftInBlock;
         while(toSkip >= BLOCK_SIZE) {
           assert posIn.getFilePointer() != lastPosBlockFP;
           forUtil.skipBlock(posIn);
+
+          if (indexHasPayloads) {
+            // Skip payloadLength block:
+            forUtil.skipBlock(payIn);
+
+            // Skip payloadBytes block:
+            int numBytes = payIn.readVInt();
+            payIn.seek(payIn.getFilePointer() + numBytes);
+          }
+
+          if (indexHasOffsets) {
+            forUtil.skipBlock(payIn);
+            forUtil.skipBlock(payIn);
+          }
           toSkip -= BLOCK_SIZE;
         }
         refillPositions();
-        posBufferUpto = toSkip;
+        payloadByteUpto = 0;
+        posBufferUpto = 0;
+        while(posBufferUpto < toSkip) {
+          if (indexHasPayloads) {
+            payloadByteUpto += payloadLengthBuffer[posBufferUpto];
+          }
+          posBufferUpto++;
+        }
       }
 
       position = 0;
+      lastStartOffset = 0;
     }
 
     @Override
     public int nextPosition() throws IOException {
-
       assert posPendingCount > 0;
 
       if (posPendingFP != -1) {
         posIn.seek(posPendingFP);
         posPendingFP = -1;
 
+        if (payPendingFP != -1 && payIn != null) {
+          payIn.seek(payPendingFP);
+          payPendingFP = -1;
+        }
+
         // Force buffer refill:
         posBufferUpto = BLOCK_SIZE;
       }
@@ -800,24 +924,44 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         refillPositions();
         posBufferUpto = 0;
       }
-      position += posDeltaBuffer[posBufferUpto++];
+      position += posDeltaBuffer[posBufferUpto];
+
+      if (indexHasPayloads) {
+        payloadLength = payloadLengthBuffer[posBufferUpto];
+        payload.bytes = payloadBytes;
+        payload.offset = payloadByteUpto;
+        payload.length = payloadLength;
+        payloadByteUpto += payloadLength;
+      }
+
+      if (indexHasOffsets) {
+        startOffset = lastStartOffset + offsetStartDeltaBuffer[posBufferUpto];
+        endOffset = startOffset + offsetLengthBuffer[posBufferUpto];
+        lastStartOffset = startOffset;
+      }
+
+      posBufferUpto++;
       posPendingCount--;
       return position;
     }
 
     @Override
     public int startOffset() {
-      return -1;
+      return startOffset;
     }
 
     @Override
     public int endOffset() {
-      return -1;
+      return endOffset;
     }
 
     @Override
     public BytesRef getPayload() {
-      return null;
+      if (payloadLength == 0) {
+        return null;
+      } else {
+        return payload;
+      }
     }
 
     @Override
@@ -826,8 +970,7 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     }
   }
 
-  // Also handles payloads + offsets
-  final class EverythingEnum extends PostingsEnum {
+  final class BlockImpactsPostingsEnum extends ImpactsEnum {
 
     private final byte[] encoded;
 
@@ -835,30 +978,13 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     private final int[] freqBuffer = new int[MAX_DATA_SIZE];
     private final int[] posDeltaBuffer = new int[MAX_DATA_SIZE];
 
-    private final int[] payloadLengthBuffer;
-    private final int[] offsetStartDeltaBuffer;
-    private final int[] offsetLengthBuffer;
-
-    private byte[] payloadBytes;
-    private int payloadByteUpto;
-    private int payloadLength;
-
-    private int lastStartOffset;
-    private int startOffset;
-    private int endOffset;
-
     private int docBufferUpto;
     private int posBufferUpto;
 
-    private Lucene50SkipReader skipper;
-    private boolean skipped;
-
-    final IndexInput startDocIn;
+    private final Lucene50ScoreSkipReader skipper;
 
-    IndexInput docIn;
+    final IndexInput docIn;
     final IndexInput posIn;
-    final IndexInput payIn;
-    final BytesRef payload;
 
     final boolean indexHasOffsets;
     final boolean indexHasPayloads;
@@ -879,10 +1005,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     // before reading positions:
     private long posPendingFP;
 
-    // Lazy pay seek: if != -1 then we must seek to this FP
-    // before reading payloads/offsets:
-    private long payPendingFP;
-
     // Where this term's postings start in the .doc file:
     private long docTermStartFP;
 
@@ -898,69 +1020,27 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     // decode vs vInt decode the block:
     private long lastPosBlockFP;
 
-    // Where this term's skip data starts (after
-    // docTermStartFP) in the .doc file (or -1 if there is
-    // no skip data for this term):
-    private long skipOffset;
-
-    private int nextSkipDoc;
+    private int nextSkipDoc = -1;
 
-    private boolean needsOffsets; // true if we actually need offsets
-    private boolean needsPayloads; // true if we actually need payloads
-    private int singletonDocID; // docid when there is a single pulsed posting, otherwise -1
+    private long seekTo = -1;
 
-    public EverythingEnum(FieldInfo fieldInfo) throws IOException {
-      this.startDocIn = Lucene50PostingsReader.this.docIn;
-      this.docIn = null;
-      this.posIn = Lucene50PostingsReader.this.posIn.clone();
-      this.payIn = Lucene50PostingsReader.this.payIn.clone();
-      encoded = new byte[MAX_ENCODED_SIZE];
+    public BlockImpactsPostingsEnum(FieldInfo fieldInfo, IntBlockTermState termState) throws IOException {
       indexHasOffsets = fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0;
-      if (indexHasOffsets) {
-        offsetStartDeltaBuffer = new int[MAX_DATA_SIZE];
-        offsetLengthBuffer = new int[MAX_DATA_SIZE];
-      } else {
-        offsetStartDeltaBuffer = null;
-        offsetLengthBuffer = null;
-        startOffset = -1;
-        endOffset = -1;
-      }
-
       indexHasPayloads = fieldInfo.hasPayloads();
-      if (indexHasPayloads) {
-        payloadLengthBuffer = new int[MAX_DATA_SIZE];
-        payloadBytes = new byte[128];
-        payload = new BytesRef();
-      } else {
-        payloadLengthBuffer = null;
-        payloadBytes = null;
-        payload = null;
-      }
-    }
 
-    public boolean canReuse(IndexInput docIn, FieldInfo fieldInfo) {
-      return docIn == startDocIn &&
-        indexHasOffsets == (fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0) &&
-        indexHasPayloads == fieldInfo.hasPayloads();
-    }
+      this.docIn = Lucene50PostingsReader.this.docIn.clone();
+
+      encoded = new byte[MAX_ENCODED_SIZE];
+
+      this.posIn = Lucene50PostingsReader.this.posIn.clone();
 
-    public EverythingEnum reset(IntBlockTermState termState, int flags) throws IOException {
       docFreq = termState.docFreq;
       docTermStartFP = termState.docStartFP;
       posTermStartFP = termState.posStartFP;
       payTermStartFP = termState.payStartFP;
-      skipOffset = termState.skipOffset;
       totalTermFreq = termState.totalTermFreq;
-      singletonDocID = termState.singletonDocID;
-      if (docFreq > 1) {
-        if (docIn == null) {
-          // lazy init
-          docIn = startDocIn.clone();
-        }
-        docIn.seek(docTermStartFP);
-      }
+      docIn.seek(docTermStartFP);
       posPendingFP = posTermStartFP;
-      payPendingFP = payTermStartFP;
       posPendingCount = 0;
       if (termState.totalTermFreq < BLOCK_SIZE) {
         lastPosBlockFP = posTermStartFP;
@@ -970,20 +1050,18 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         lastPosBlockFP = posTermStartFP + termState.lastPosBlockOffset;
       }
 
-      this.needsOffsets = PostingsEnum.featureRequested(flags, PostingsEnum.OFFSETS);
-      this.needsPayloads = PostingsEnum.featureRequested(flags, PostingsEnum.PAYLOADS);
-
       doc = -1;
       accum = 0;
       docUpto = 0;
-      if (docFreq > BLOCK_SIZE) {
-        nextSkipDoc = BLOCK_SIZE - 1; // we won't skip if target is found in first block
-      } else {
-        nextSkipDoc = NO_MORE_DOCS; // not enough docs for skipping
-      }
       docBufferUpto = BLOCK_SIZE;
-      skipped = false;
-      return this;
+
+      skipper = new Lucene50ScoreSkipReader(version,
+          docIn.clone(),
+          MAX_SKIP_LEVELS,
+          true,
+          indexHasOffsets,
+          indexHasPayloads);
+      skipper.init(docTermStartFP+termState.skipOffset, docTermStartFP, posTermStartFP, payTermStartFP, docFreq);
     }
 
     @Override
@@ -1003,9 +1081,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
       if (left >= BLOCK_SIZE) {
         forUtil.readBlock(docIn, encoded, docDeltaBuffer);
         forUtil.readBlock(docIn, encoded, freqBuffer);
-      } else if (docFreq == 1) {
-        docDeltaBuffer[0] = singletonDocID;
-        freqBuffer[0] = (int) totalTermFreq;
       } else {
         readVIntBlock(docIn, docDeltaBuffer, freqBuffer, left, true);
       }
@@ -1016,115 +1091,36 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
       if (posIn.getFilePointer() == lastPosBlockFP) {
         final int count = (int) (totalTermFreq % BLOCK_SIZE);
         int payloadLength = 0;
-        int offsetLength = 0;
-        payloadByteUpto = 0;
         for(int i=0;i<count;i++) {
           int code = posIn.readVInt();
           if (indexHasPayloads) {
             if ((code & 1) != 0) {
               payloadLength = posIn.readVInt();
             }
-            payloadLengthBuffer[i] = payloadLength;
             posDeltaBuffer[i] = code >>> 1;
             if (payloadLength != 0) {
-              if (payloadByteUpto + payloadLength > payloadBytes.length) {
-                payloadBytes = ArrayUtil.grow(payloadBytes, payloadByteUpto + payloadLength);
-              }
-              posIn.readBytes(payloadBytes, payloadByteUpto, payloadLength);
-              payloadByteUpto += payloadLength;
+              posIn.seek(posIn.getFilePointer() + payloadLength);
             }
           } else {
             posDeltaBuffer[i] = code;
           }
-
           if (indexHasOffsets) {
-            int deltaCode = posIn.readVInt();
-            if ((deltaCode & 1) != 0) {
-              offsetLength = posIn.readVInt();
+            if ((posIn.readVInt() & 1) != 0) {
+              // offset length changed
+              posIn.readVInt();
             }
-            offsetStartDeltaBuffer[i] = deltaCode >>> 1;
-            offsetLengthBuffer[i] = offsetLength;
           }
         }
-        payloadByteUpto = 0;
       } else {
         forUtil.readBlock(posIn, encoded, posDeltaBuffer);
-
-        if (indexHasPayloads) {
-          if (needsPayloads) {
-            forUtil.readBlock(payIn, encoded, payloadLengthBuffer);
-            int numBytes = payIn.readVInt();
-
-            if (numBytes > payloadBytes.length) {
-              payloadBytes = ArrayUtil.grow(payloadBytes, numBytes);
-            }
-            payIn.readBytes(payloadBytes, 0, numBytes);
-          } else {
-            // this works, because when writing a vint block we always force the first length to be written
-            forUtil.skipBlock(payIn); // skip over lengths
-            int numBytes = payIn.readVInt(); // read length of payloadBytes
-            payIn.seek(payIn.getFilePointer() + numBytes); // skip over payloadBytes
-          }
-          payloadByteUpto = 0;
-        }
-
-        if (indexHasOffsets) {
-          if (needsOffsets) {
-            forUtil.readBlock(payIn, encoded, offsetStartDeltaBuffer);
-            forUtil.readBlock(payIn, encoded, offsetLengthBuffer);
-          } else {
-            // this works, because when writing a vint block we always force the first length to be written
-            forUtil.skipBlock(payIn); // skip over starts
-            forUtil.skipBlock(payIn); // skip over lengths
-          }
-        }
       }
     }
 
     @Override
-    public int nextDoc() throws IOException {
-      if (docUpto == docFreq) {
-        return doc = NO_MORE_DOCS;
-      }
-      if (docBufferUpto == BLOCK_SIZE) {
-        refillDocs();
-      }
-
-      accum += docDeltaBuffer[docBufferUpto];
-      freq = freqBuffer[docBufferUpto];
-      posPendingCount += freq;
-      docBufferUpto++;
-      docUpto++;
-
-      doc = accum;
-      position = 0;
-      lastStartOffset = 0;
-      return doc;
-    }
-
-    @Override
-    public int advance(int target) throws IOException {
-      // TODO: make frq block load lazy/skippable
-
+    public void advanceShallow(int target) throws IOException {
       if (target > nextSkipDoc) {
-        if (skipper == null) {
-          // Lazy init: first time this enum has ever been used for skipping
-          skipper = new Lucene50SkipReader(version,
-                                        docIn.clone(),
-                                        MAX_SKIP_LEVELS,
-                                        true,
-                                        indexHasOffsets,
-                                        indexHasPayloads);
-        }
-
-        if (!skipped) {
-          assert skipOffset != -1;
-          // This is the first time this enum has skipped
-          // since reset() was called; load the skip data:
-          skipper.init(docTermStartFP+skipOffset, docTermStartFP, posTermStartFP, payTermStartFP, docFreq);
-          skipped = true;
-        }
-
+        // always plus one to fix the result, since skip position in Lucene50SkipReader
+        // is a little different from MultiLevelSkipListReader
         final int newDocUpto = skipper.skipTo(target) + 1;
 
         if (newDocUpto > docUpto) {
@@ -1135,19 +1131,41 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
           // Force to read next block
           docBufferUpto = BLOCK_SIZE;
           accum = skipper.getDoc();
-          docIn.seek(skipper.getDocPointer());
           posPendingFP = skipper.getPosPointer();
-          payPendingFP = skipper.getPayPointer();
           posPendingCount = skipper.getPosBufferUpto();
-          lastStartOffset = 0; // new document
-          payloadByteUpto = skipper.getPayloadByteUpto();
+          seekTo = skipper.getDocPointer();       // delay the seek
         }
+        // next time we call advance, this is used to 
+        // foresee whether skipper is necessary.
         nextSkipDoc = skipper.getNextSkipDoc();
       }
+      assert nextSkipDoc >= target;
+    }
+
+    @Override
+    public Impacts getImpacts() throws IOException {
+      advanceShallow(doc);
+      return skipper.getImpacts();
+    }
+
+    @Override
+    public int nextDoc() throws IOException {
+      return advance(doc + 1);
+    }
+
+    @Override
+    public int advance(int target) throws IOException {
+      if (target > nextSkipDoc) {
+        advanceShallow(target);
+      }
       if (docUpto == docFreq) {
         return doc = NO_MORE_DOCS;
       }
       if (docBufferUpto == BLOCK_SIZE) {
+        if (seekTo >= 0) {
+          docIn.seek(seekTo);
+          seekTo = -1;
+        }
         refillDocs();
       }
 
@@ -1166,9 +1184,8 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
           return doc = NO_MORE_DOCS;
         }
       }
-
       position = 0;
-      lastStartOffset = 0;
+
       return doc = accum;
     }
 
@@ -1179,53 +1196,22 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
     private void skipPositions() throws IOException {
       // Skip positions now:
       int toSkip = posPendingCount - freq;
-      // if (DEBUG) {
-      //   System.out.println("      FPR.skipPositions: toSkip=" + toSkip);
-      // }
 
       final int leftInBlock = BLOCK_SIZE - posBufferUpto;
       if (toSkip < leftInBlock) {
-        int end = posBufferUpto + toSkip;
-        while(posBufferUpto < end) {
-          if (indexHasPayloads) {
-            payloadByteUpto += payloadLengthBuffer[posBufferUpto];
-          }
-          posBufferUpto++;
-        }
+        posBufferUpto += toSkip;
       } else {
         toSkip -= leftInBlock;
         while(toSkip >= BLOCK_SIZE) {
           assert posIn.getFilePointer() != lastPosBlockFP;
           forUtil.skipBlock(posIn);
-
-          if (indexHasPayloads) {
-            // Skip payloadLength block:
-            forUtil.skipBlock(payIn);
-
-            // Skip payloadBytes block:
-            int numBytes = payIn.readVInt();
-            payIn.seek(payIn.getFilePointer() + numBytes);
-          }
-
-          if (indexHasOffsets) {
-            forUtil.skipBlock(payIn);
-            forUtil.skipBlock(payIn);
-          }
           toSkip -= BLOCK_SIZE;
         }
         refillPositions();
-        payloadByteUpto = 0;
-        posBufferUpto = 0;
-        while(posBufferUpto < toSkip) {
-          if (indexHasPayloads) {
-            payloadByteUpto += payloadLengthBuffer[posBufferUpto];
-          }
-          posBufferUpto++;
-        }
+        posBufferUpto = toSkip;
       }
 
       position = 0;
-      lastStartOffset = 0;
     }
 
     @Override
@@ -1236,11 +1222,6 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         posIn.seek(posPendingFP);
         posPendingFP = -1;
 
-        if (payPendingFP != -1) {
-          payIn.seek(payPendingFP);
-          payPendingFP = -1;
-        }
-
         // Force buffer refill:
         posBufferUpto = BLOCK_SIZE;
       }
@@ -1254,52 +1235,33 @@ public final class Lucene50PostingsReader extends PostingsReaderBase {
         refillPositions();
         posBufferUpto = 0;
       }
-      position += posDeltaBuffer[posBufferUpto];
-
-      if (indexHasPayloads) {
-        payloadLength = payloadLengthBuffer[posBufferUpto];
-        payload.bytes = payloadBytes;
-        payload.offset = payloadByteUpto;
-        payload.length = payloadLength;
-        payloadByteUpto += payloadLength;
-      }
-
-      if (indexHasOffsets) {
-        startOffset = lastStartOffset + offsetStartDeltaBuffer[posBufferUpto];
-        endOffset = startOffset + offsetLengthBuffer[posBufferUpto];
-        lastStartOffset = startOffset;
-      }
+      position += posDeltaBuffer[posBufferUpto++];
 
-      posBufferUpto++;
       posPendingCount--;
       return position;
     }
 
     @Override
     public int startOffset() {
-      return startOffset;
+      return -1;
     }
 
     @Override
     public int endOffset() {
-      return endOffset;
+      return -1;
     }
 
     @Override
     public BytesRef getPayload() {
-      if (payloadLength == 0) {
-        return null;
-      } else {
-        return payload;
-      }
+      return null;
     }
 
     @Override
     public long cost() {
       return docFreq;
     }
-  }
 
+  }
 
   final class BlockImpactsEverythingEnum extends ImpactsEnum {
     
diff --git a/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java b/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java
index b95077d..2de5b09 100644
--- a/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java
+++ b/lucene/core/src/java/org/apache/lucene/search/ExactPhraseMatcher.java
@@ -19,9 +19,19 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.stream.Collectors;
 
+import org.apache.lucene.index.Impact;
+import org.apache.lucene.index.Impacts;
+import org.apache.lucene.index.ImpactsEnum;
+import org.apache.lucene.index.ImpactsSource;
 import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.search.similarities.Similarity.SimScorer;
+import org.apache.lucene.util.PriorityQueue;
 
 final class ExactPhraseMatcher extends PhraseMatcher {
 
@@ -37,9 +47,21 @@ final class ExactPhraseMatcher extends PhraseMatcher {
   }
 
   private final PostingsAndPosition[] postings;
+  private final DocIdSetIterator approximation;
+  private final ImpactsDISI impactsApproximation;
 
-  ExactPhraseMatcher(PhraseQuery.PostingsAndFreq[] postings, float matchCost) {
-    super(approximation(postings), matchCost);
+  ExactPhraseMatcher(PhraseQuery.PostingsAndFreq[] postings, ScoreMode scoreMode, SimScorer scorer, float matchCost) {
+    super(matchCost);
+
+    final DocIdSetIterator approximation = ConjunctionDISI.intersectIterators(Arrays.stream(postings).map(p -> p.postings).collect(Collectors.toList()));
+    final ImpactsSource impactsSource = mergeImpacts(Arrays.stream(postings).map(p -> p.impacts).toArray(ImpactsEnum[]::new));
+
+    if (scoreMode == ScoreMode.TOP_SCORES) {
+      this.approximation = this.impactsApproximation = new ImpactsDISI(approximation, impactsSource, scorer);
+    } else {
+      this.approximation = approximation;
+      this.impactsApproximation = new ImpactsDISI(approximation, impactsSource, scorer);
+    }
 
     List<PostingsAndPosition> postingsAndPositions = new ArrayList<>();
     for(PhraseQuery.PostingsAndFreq posting : postings) {
@@ -48,12 +70,14 @@ final class ExactPhraseMatcher extends PhraseMatcher {
     this.postings = postingsAndPositions.toArray(new PostingsAndPosition[postingsAndPositions.size()]);
   }
 
-  private static DocIdSetIterator approximation(PhraseQuery.PostingsAndFreq[] postings) {
-    List<DocIdSetIterator> iterators = new ArrayList<>();
-    for (PhraseQuery.PostingsAndFreq posting : postings) {
-      iterators.add(posting.postings);
-    }
-    return ConjunctionDISI.intersectIterators(iterators);
+  @Override
+  DocIdSetIterator approximation() {
+    return approximation;
+  }
+
+  @Override
+  ImpactsDISI impactsApproximation() {
+    return impactsApproximation;
   }
 
   @Override
@@ -149,4 +173,173 @@ final class ExactPhraseMatcher extends PhraseMatcher {
     return postings[postings.length - 1].postings.endOffset();
   }
 
+  /**
+   * Merge impacts for multiple terms of an exact phrase.
+   */
+  static ImpactsSource mergeImpacts(ImpactsEnum[] impactsEnums) {
+    // Iteration of block boundaries uses the impacts enum with the lower cost.
+    // This is consistent with BlockMaxConjunctionScorer.
+    int tmpLeadIndex = -1;
+    for (int i = 0; i < impactsEnums.length; ++i) {
+      if (tmpLeadIndex == -1 || impactsEnums[i].cost() < impactsEnums[tmpLeadIndex].cost()) {
+        tmpLeadIndex = i;
+      }
+    }
+    final int leadIndex = tmpLeadIndex;
+
+    return new ImpactsSource() {
+
+      class SubIterator {
+        final Iterator<Impact> iterator;
+        Impact current;
+
+        SubIterator(List<Impact> impacts) {
+          this.iterator = impacts.iterator();
+          this.current = iterator.next();
+        }
+
+        boolean next() {
+          if (iterator.hasNext() == false) {
+            current = null;
+            return false;
+          } else {
+            current = iterator.next();
+            return true;
+          }
+        }
+      }
+
+      @Override
+      public Impacts getImpacts() throws IOException {
+        final Impacts[] impacts = new Impacts[impactsEnums.length];
+        for (int i = 0; i < impactsEnums.length; ++i) {
+          impacts[i] = impactsEnums[i].getImpacts();
+        }
+        final Impacts lead = impacts[leadIndex];
+        return new Impacts() {
+
+          @Override
+          public int numLevels() {
+            // Delegate to the lead
+            return lead.numLevels();
+          }
+
+          @Override
+          public int getDocIdUpTo(int level) {
+            // Delegate to the lead
+            return lead.getDocIdUpTo(level);
+          }
+
+          /**
+           * Return the minimum level whose impacts are valid up to {@code docIdUpTo},
+           * or {@code -1} if there is no such level.
+           */
+          private int getLevel(Impacts impacts, int docIdUpTo) {
+            for (int level = 0, numLevels = impacts.numLevels(); level < numLevels; ++level) {
+              if (impacts.getDocIdUpTo(level) >= docIdUpTo) {
+                return level;
+              }
+            }
+            return -1;
+          }
+
+          @Override
+          public List<Impact> getImpacts(int level) {
+            final int docIdUpTo = getDocIdUpTo(level);
+
+            PriorityQueue<SubIterator> pq = new PriorityQueue<SubIterator>(impacts.length) {
+              @Override
+              protected boolean lessThan(SubIterator a, SubIterator b) {
+                return a.current.freq < b.current.freq;
+              }
+            };
+
+            boolean hasImpacts = false;
+            List<Impact> onlyImpactList = null;
+            for (int i = 0; i < impacts.length; ++i) {
+              int impactsLevel = getLevel(impacts[i], docIdUpTo);
+              if (impactsLevel == -1) {
+                // This instance doesn't have useful impacts, ignore it: this is safe.
+                continue;
+              }
+
+              List<Impact> impactList = impacts[i].getImpacts(impactsLevel);
+              Impact firstImpact = impactList.get(0);
+              if (firstImpact.freq == Integer.MAX_VALUE && firstImpact.norm == 1L) {
+                // Dummy impacts, ignore it too.
+                continue;
+              }
+
+              SubIterator subIterator = new SubIterator(impactList);
+              pq.add(subIterator);
+              if (hasImpacts == false) {
+                hasImpacts = true;
+                onlyImpactList = impactList;
+              } else {
+                onlyImpactList = null; // there are multiple impacts
+              }
+            }
+
+            if (hasImpacts == false) {
+              return Collections.singletonList(new Impact(Integer.MAX_VALUE, 1L));
+            } else if (onlyImpactList != null) {
+              return onlyImpactList;
+            }
+
+            // Idea: merge impacts by freq. The tricky thing is that we need to
+            // consider freq values that are not in the impacts too. For
+            // instance if the list of impacts is [{freq=2,norm=10}, {freq=4,norm=12}],
+            // there might well be a document that has a freq of 2 and a length of 11,
+            // which was just not added to the list of impacts because {freq=2,norm=10}
+            // is more competitive.
+            // We walk impacts in parallel through a PQ ordered by freq. At any time,
+            // the competitive impact consists of the lowest freq among all entries of
+            // the PQ (the top) and the highest norm (tracked separately).
+            List<Impact> mergedImpacts = new ArrayList<>();
+            SubIterator top = pq.top();
+            int currentFreq = top.current.freq;
+            long currentNorm = 0;
+            for (SubIterator it : pq) {
+              if (Long.compareUnsigned(it.current.norm, currentNorm) > 0) {
+                currentNorm = it.current.norm;
+              }
+            }
+
+            outer: while (true) {
+              if (mergedImpacts.size() > 0 && mergedImpacts.get(mergedImpacts.size() - 1).norm == currentNorm) {
+                mergedImpacts.get(mergedImpacts.size() - 1).freq = currentFreq;
+              } else {
+                mergedImpacts.add(new Impact(currentFreq, currentNorm));
+              }
+
+              do {
+                if (top.next() == false) {
+                  // At least one clause doesn't have any more documents below the current norm,
+                  // so we can safely ignore further clauses. The only reason why they have more
+                  // impacts is because they cover more documents that we are not interested in.
+                  break outer;
+                }
+                if (Long.compareUnsigned(top.current.norm, currentNorm) > 0) {
+                  currentNorm = top.current.norm;
+                }
+                top = pq.updateTop();
+              } while (top.current.freq == currentFreq);
+
+              currentFreq = top.current.freq;
+            }
+
+            return mergedImpacts;
+          }
+        };
+      }
+
+      @Override
+      public void advanceShallow(int target) throws IOException {
+        for (ImpactsEnum impactsEnum : impactsEnums) {
+          impactsEnum.advanceShallow(target);
+        }
+      }
+    };
+  }
+
 }
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 4aafa7b..5f3b234 100644
--- a/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java
@@ -31,12 +31,14 @@ import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.SlowImpactsEnum;
 import org.apache.lucene.index.Term;
 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.similarities.Similarity;
+import org.apache.lucene.search.similarities.Similarity.SimScorer;
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.PriorityQueue;
@@ -250,7 +252,7 @@ public class MultiPhraseQuery extends Query {
       }
 
       @Override
-      protected PhraseMatcher getPhraseMatcher(LeafReaderContext context, boolean exposeOffsets) throws IOException {
+      protected PhraseMatcher getPhraseMatcher(LeafReaderContext context, SimScorer scorer, boolean exposeOffsets) throws IOException {
         assert termArrays.length != 0;
         final LeafReader reader = context.reader();
 
@@ -295,16 +297,16 @@ public class MultiPhraseQuery extends Query {
             postingsEnum = exposeOffsets ? new UnionFullPostingsEnum(postings) : new UnionPostingsEnum(postings);
           }
 
-          postingsFreqs[pos] = new PhraseQuery.PostingsAndFreq(postingsEnum, positions[pos], terms);
+          postingsFreqs[pos] = new PhraseQuery.PostingsAndFreq(postingsEnum, new SlowImpactsEnum(postingsEnum), positions[pos], terms);
         }
 
         // sort by increasing docFreq order
         if (slop == 0) {
           ArrayUtil.timSort(postingsFreqs);
-          return new ExactPhraseMatcher(postingsFreqs, totalMatchCost);
+          return new ExactPhraseMatcher(postingsFreqs, scoreMode, scorer, totalMatchCost);
         }
         else {
-          return new SloppyPhraseMatcher(postingsFreqs, slop, totalMatchCost, exposeOffsets);
+          return new SloppyPhraseMatcher(postingsFreqs, slop, scoreMode, scorer, totalMatchCost, exposeOffsets);
         }
 
       }
diff --git a/lucene/core/src/java/org/apache/lucene/search/PhraseMatcher.java b/lucene/core/src/java/org/apache/lucene/search/PhraseMatcher.java
index a3cbd34..c8ba3dd 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PhraseMatcher.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PhraseMatcher.java
@@ -28,16 +28,23 @@ import java.io.IOException;
  */
 abstract class PhraseMatcher {
 
-  protected final DocIdSetIterator approximation;
   private final float matchCost;
 
-  PhraseMatcher(DocIdSetIterator approximation, float matchCost) {
-    assert TwoPhaseIterator.unwrap(approximation) == null;
-    this.approximation = approximation;
+  PhraseMatcher(float matchCost) {
     this.matchCost = matchCost;
   }
 
   /**
+   * Approximation that only matches documents that have all terms.
+   */
+  abstract DocIdSetIterator approximation();
+
+  /**
+   * Approximation that is aware of impacts.
+   */
+  abstract ImpactsDISI impactsApproximation();
+
+  /**
    * An upper bound on the number of possible matches on this document
    */
   abstract float maxFreq() throws IOException;
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 271fed6..6c6460d 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java
@@ -24,17 +24,20 @@ import java.util.List;
 
 import org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat;
 import org.apache.lucene.codecs.lucene50.Lucene50PostingsReader;
+import org.apache.lucene.index.ImpactsEnum;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexReaderContext;
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.SlowImpactsEnum;
 import org.apache.lucene.index.Term;
 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.similarities.Similarity;
+import org.apache.lucene.search.similarities.Similarity.SimScorer;
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.BytesRef;
 
@@ -296,12 +299,14 @@ public class PhraseQuery extends Query {
 
   static class PostingsAndFreq implements Comparable<PostingsAndFreq> {
     final PostingsEnum postings;
+    final ImpactsEnum impacts;
     final int position;
     final Term[] terms;
     final int nTerms; // for faster comparisons
 
-    public PostingsAndFreq(PostingsEnum postings, int position, Term... terms) {
+    public PostingsAndFreq(PostingsEnum postings, ImpactsEnum impacts, int position, Term... terms) {
       this.postings = postings;
+      this.impacts = impacts;
       this.position = position;
       nTerms = terms==null ? 0 : terms.length;
       if (nTerms>0) {
@@ -362,7 +367,7 @@ public class PhraseQuery extends Query {
   /** A guess of
    * the average number of simple operations for the initial seek and buffer refill
    * per document for the positions of a term.
-   * See also {@link Lucene50PostingsReader.BlockPostingsEnum#nextPosition()}.
+   * See also {@link Lucene50PostingsReader.BlockImpactsPostingsEnum#nextPosition()}.
    * <p>
    * Aside: Instead of being constant this could depend among others on
    * {@link Lucene50PostingsFormat#BLOCK_SIZE},
@@ -374,7 +379,7 @@ public class PhraseQuery extends Query {
    */
   private static final int TERM_POSNS_SEEK_OPS_PER_DOC = 128;
 
-  /** Number of simple operations in {@link Lucene50PostingsReader.BlockPostingsEnum#nextPosition()}
+  /** Number of simple operations in {@link Lucene50PostingsReader.BlockImpactsPostingsEnum#nextPosition()}
    *  when no seek or buffer refill is done.
    */
   private static final int TERM_OPS_PER_POS = 7;
@@ -430,7 +435,7 @@ public class PhraseQuery extends Query {
       }
 
       @Override
-      protected PhraseMatcher getPhraseMatcher(LeafReaderContext context, boolean exposeOffsets) throws IOException {
+      protected PhraseMatcher getPhraseMatcher(LeafReaderContext context, SimScorer scorer, boolean exposeOffsets) throws IOException {
         assert terms.length > 0;
         final LeafReader reader = context.reader();
         PostingsAndFreq[] postingsFreqs = new PostingsAndFreq[terms.length];
@@ -456,18 +461,25 @@ public class PhraseQuery extends Query {
             return null;
           }
           te.seekExact(t.bytes(), state);
-          PostingsEnum postingsEnum = te.postings(null, exposeOffsets ? PostingsEnum.ALL : PostingsEnum.POSITIONS);
-          postingsFreqs[i] = new PostingsAndFreq(postingsEnum, positions[i], t);
+          PostingsEnum postingsEnum;
+          ImpactsEnum impactsEnum;
+          if (scoreMode == ScoreMode.TOP_SCORES) {
+            postingsEnum = impactsEnum = te.impacts(exposeOffsets ? PostingsEnum.OFFSETS : PostingsEnum.POSITIONS);
+          } else {
+            postingsEnum = te.postings(null, exposeOffsets ? PostingsEnum.OFFSETS : PostingsEnum.POSITIONS);
+            impactsEnum = new SlowImpactsEnum(postingsEnum);
+          }
+          postingsFreqs[i] = new PostingsAndFreq(postingsEnum, impactsEnum, positions[i], t);
           totalMatchCost += termPositionsCost(te);
         }
 
         // sort by increasing docFreq order
         if (slop == 0) {
           ArrayUtil.timSort(postingsFreqs);
-          return new ExactPhraseMatcher(postingsFreqs, totalMatchCost);
+          return new ExactPhraseMatcher(postingsFreqs, scoreMode, scorer, totalMatchCost);
         }
         else {
-          return new SloppyPhraseMatcher(postingsFreqs, slop, totalMatchCost, exposeOffsets);
+          return new SloppyPhraseMatcher(postingsFreqs, slop, scoreMode, scorer, totalMatchCost, exposeOffsets);
         }
       }
     };
diff --git a/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java b/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java
index 807a9dd..2bcf737 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PhraseScorer.java
@@ -21,6 +21,8 @@ import java.io.IOException;
 
 class PhraseScorer extends Scorer {
 
+  final DocIdSetIterator approximation;
+  final ImpactsDISI impactsApproximation;
   final PhraseMatcher matcher;
   final ScoreMode scoreMode;
   private final LeafSimScorer simScorer;
@@ -35,11 +37,13 @@ class PhraseScorer extends Scorer {
     this.scoreMode = scoreMode;
     this.simScorer = simScorer;
     this.matchCost = matcher.getMatchCost();
+    this.approximation = matcher.approximation();
+    this.impactsApproximation = matcher.impactsApproximation();
   }
 
   @Override
   public TwoPhaseIterator twoPhaseIterator() {
-    return new TwoPhaseIterator(matcher.approximation) {
+    return new TwoPhaseIterator(approximation) {
       @Override
       public boolean matches() throws IOException {
         matcher.reset();
@@ -63,7 +67,7 @@ class PhraseScorer extends Scorer {
 
   @Override
   public int docID() {
-    return matcher.approximation.docID();
+    return approximation.docID();
   }
 
   @Override
@@ -85,12 +89,17 @@ class PhraseScorer extends Scorer {
   @Override
   public void setMinCompetitiveScore(float minScore) {
     this.minCompetitiveScore = minScore;
+    impactsApproximation.setMinCompetitiveScore(minScore);
+  }
+
+  @Override
+  public int advanceShallow(int target) throws IOException {
+    return impactsApproximation.advanceShallow(target);
   }
 
   @Override
   public float getMaxScore(int upTo) throws IOException {
-    // TODO: merge impacts of all clauses to get better score upper bounds
-    return simScorer.getSimScorer().score(Integer.MAX_VALUE, 1L);
+    return impactsApproximation.getMaxScore(upTo);
   }
 
   @Override
@@ -98,5 +107,4 @@ class PhraseScorer extends Scorer {
     return "PhraseScorer(" + weight + ")";
   }
 
-
 }
diff --git a/lucene/core/src/java/org/apache/lucene/search/PhraseWeight.java b/lucene/core/src/java/org/apache/lucene/search/PhraseWeight.java
index 30b01db..64c413d 100644
--- a/lucene/core/src/java/org/apache/lucene/search/PhraseWeight.java
+++ b/lucene/core/src/java/org/apache/lucene/search/PhraseWeight.java
@@ -49,11 +49,11 @@ abstract class PhraseWeight extends Weight {
 
   protected abstract Similarity.SimScorer getStats(IndexSearcher searcher) throws IOException;
 
-  protected abstract PhraseMatcher getPhraseMatcher(LeafReaderContext context, boolean exposeOffsets) throws IOException;
+  protected abstract PhraseMatcher getPhraseMatcher(LeafReaderContext context, SimScorer scorer, boolean exposeOffsets) throws IOException;
 
   @Override
   public Scorer scorer(LeafReaderContext context) throws IOException {
-    PhraseMatcher matcher = getPhraseMatcher(context, false);
+    PhraseMatcher matcher = getPhraseMatcher(context, stats, false);
     if (matcher == null)
       return null;
     LeafSimScorer simScorer = new LeafSimScorer(stats, context.reader(), field, scoreMode.needsScores());
@@ -62,8 +62,8 @@ abstract class PhraseWeight extends Weight {
 
   @Override
   public Explanation explain(LeafReaderContext context, int doc) throws IOException {
-    PhraseMatcher matcher = getPhraseMatcher(context, false);
-    if (matcher == null || matcher.approximation.advance(doc) != doc) {
+    PhraseMatcher matcher = getPhraseMatcher(context, stats, false);
+    if (matcher == null || matcher.approximation().advance(doc) != doc) {
       return Explanation.noMatch("no matching terms");
     }
     matcher.reset();
@@ -86,8 +86,8 @@ abstract class PhraseWeight extends Weight {
   @Override
   public Matches matches(LeafReaderContext context, int doc) throws IOException {
     return MatchesUtils.forField(field, () -> {
-      PhraseMatcher matcher = getPhraseMatcher(context, true);
-      if (matcher == null || matcher.approximation.advance(doc) != doc) {
+      PhraseMatcher matcher = getPhraseMatcher(context, stats, true);
+      if (matcher == null || matcher.approximation().advance(doc) != doc) {
         return null;
       }
       matcher.reset();
diff --git a/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java b/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java
index bfe0682..c4da423 100644
--- a/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java
+++ b/lucene/core/src/java/org/apache/lucene/search/SloppyPhraseMatcher.java
@@ -20,13 +20,19 @@ package org.apache.lucene.search;
 import java.io.IOException;
 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.LinkedHashMap;
 import java.util.List;
+import java.util.stream.Collectors;
 
+import org.apache.lucene.index.Impact;
+import org.apache.lucene.index.Impacts;
+import org.apache.lucene.index.ImpactsSource;
 import org.apache.lucene.index.Term;
+import org.apache.lucene.search.similarities.Similarity.SimScorer;
 import org.apache.lucene.util.FixedBitSet;
 
 /**
@@ -56,6 +62,9 @@ final class SloppyPhraseMatcher extends PhraseMatcher {
   private final PhraseQueue pq; // for advancing min position
   private final boolean captureLeadMatch;
 
+  private final DocIdSetIterator approximation;
+  private final ImpactsDISI impactsApproximation;
+
   private int end; // current largest phrase position
 
   private int leadPosition;
@@ -72,8 +81,8 @@ final class SloppyPhraseMatcher extends PhraseMatcher {
   private boolean positioned;
   private int matchLength;
 
-  SloppyPhraseMatcher(PhraseQuery.PostingsAndFreq[] postings, int slop, float matchCost, boolean captureLeadMatch) {
-    super(approximation(postings), matchCost);
+  SloppyPhraseMatcher(PhraseQuery.PostingsAndFreq[] postings, int slop, ScoreMode scoreMode, SimScorer scorer, float matchCost, boolean captureLeadMatch) {
+    super(matchCost);
     this.slop = slop;
     this.numPostings = postings.length;
     this.captureLeadMatch = captureLeadMatch;
@@ -82,14 +91,49 @@ final class SloppyPhraseMatcher extends PhraseMatcher {
     for (int i = 0; i < postings.length; ++i) {
       phrasePositions[i] = new PhrasePositions(postings[i].postings, postings[i].position, i, postings[i].terms);
     }
+
+    approximation = ConjunctionDISI.intersectIterators(Arrays.stream(postings).map(p -> p.postings).collect(Collectors.toList()));
+    // What would be a good upper bound of the sloppy frequency? A sum of the
+    // sub frequencies would be correct, but it is usually so much higher than
+    // the actual sloppy frequency that it doesn't help skip irrelevant
+    // documents. As a consequence for now, sloppy phrase queries use dummy
+    // impacts:
+    final ImpactsSource impactsSource = new ImpactsSource() {
+      @Override
+      public Impacts getImpacts() throws IOException {
+        return new Impacts() {
+
+          @Override
+          public int numLevels() {
+            return 1;
+          }
+
+          @Override
+          public List<Impact> getImpacts(int level) {
+            return Collections.singletonList(new Impact(Integer.MAX_VALUE, 1L));
+          }
+
+          @Override
+          public int getDocIdUpTo(int level) {
+            return DocIdSetIterator.NO_MORE_DOCS;
+          }
+        };
+      }
+
+      @Override
+      public void advanceShallow(int target) throws IOException {}
+    };
+    impactsApproximation = new ImpactsDISI(approximation, impactsSource, scorer);
   }
 
-  private static DocIdSetIterator approximation(PhraseQuery.PostingsAndFreq[] postings) {
-    List<DocIdSetIterator> iterators = new ArrayList<>();
-    for (PhraseQuery.PostingsAndFreq posting : postings) {
-      iterators.add(posting.postings);
-    }
-    return ConjunctionDISI.intersectIterators(iterators);
+  @Override
+  DocIdSetIterator approximation() {
+    return approximation;
+  }
+
+  @Override
+  ImpactsDISI impactsApproximation() {
+    return impactsApproximation;
   }
 
   @Override
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestPhraseQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestPhraseQuery.java
index e25a159..d6529a6 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestPhraseQuery.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestPhraseQuery.java
@@ -23,6 +23,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.CannedTokenStream;
@@ -34,18 +36,24 @@ import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
-import org.apache.lucene.document.TextField;
 import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.TextField;
 import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.Impact;
+import org.apache.lucene.index.Impacts;
+import org.apache.lucene.index.ImpactsEnum;
+import org.apache.lucene.index.ImpactsSource;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
 import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.similarities.BM25Similarity;
 import org.apache.lucene.search.similarities.ClassicSimilarity;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
 import org.junit.AfterClass;
@@ -761,4 +769,306 @@ public class TestPhraseQuery extends LuceneTestCase {
     r.close();
     dir.close();
   }
+
+  public void testMergeImpacts() throws IOException {
+    DummyImpactsEnum impacts1 = new DummyImpactsEnum(1000);
+    DummyImpactsEnum impacts2 = new DummyImpactsEnum(2000);
+    ImpactsSource mergedImpacts = ExactPhraseMatcher.mergeImpacts(new ImpactsEnum[] { impacts1, impacts2 });
+
+    impacts1.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(5, 12), new Impact(8, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(8, 13),  new Impact(12, 14) }
+        },
+        new int[] {
+            110,
+            945
+        });
+
+    // Merge with empty impacts
+    impacts2.reset(
+        new Impact[0][],
+        new int[0]);
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(5, 12), new Impact(8, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(8, 13),  new Impact(12, 14) }
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+
+    // Merge with dummy impacts
+    impacts2.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(Integer.MAX_VALUE, 1) }
+        },
+        new int[] {
+            5000
+        });
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(5, 12), new Impact(8, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(8, 13),  new Impact(12, 14) }
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+
+    // Merge with dummy impacts that we don't special case
+    impacts2.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(Integer.MAX_VALUE, 2) }
+        },
+        new int[] {
+            5000
+        });
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(5, 12), new Impact(8, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(8, 13),  new Impact(12, 14) }
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+
+    // First level of impacts2 doesn't cover the first level of impacts1
+    impacts2.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(2, 10), new Impact(6, 13) },
+          new Impact[] { new Impact(3, 9), new Impact(5, 11), new Impact(7, 13) }
+        },
+        new int[] {
+            90,
+            1000
+        }); 
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(5, 12), new Impact(7, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(7, 13) }
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+
+    // Second level of impacts2 doesn't cover the first level of impacts1
+    impacts2.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(2, 10), new Impact(6, 11) },
+          new Impact[] { new Impact(3, 9), new Impact(5, 11), new Impact(7, 13) }
+        },
+        new int[] {
+            150,
+            900
+        });
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(2, 10), new Impact(3, 11), new Impact(5, 12), new Impact(6, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(8, 13),  new Impact(12, 14) } // same as impacts1
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+
+    impacts2.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(4, 10), new Impact(9, 13) },
+          new Impact[] { new Impact(1, 1), new Impact(4, 10), new Impact(5, 11), new Impact(8, 13), new Impact(12, 14), new Impact(13, 15) }
+        },
+        new int[] {
+            113,
+            950
+        });
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(4, 12), new Impact(8, 13) },
+          new Impact[] { new Impact(3, 10), new Impact(5, 11), new Impact(8, 13), new Impact(12, 14) }
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+
+    // Make sure negative norms are treated as unsigned
+    impacts1.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(3, 10), new Impact(5, -10), new Impact(8, -5) },
+          new Impact[] { new Impact(3, 10), new Impact(5, -15), new Impact(8, -5),  new Impact(12, -3) }
+        },
+        new int[] {
+            110,
+            945
+        });
+    impacts2.reset(
+        new Impact[][] {
+          new Impact[] { new Impact(2, 10), new Impact(12, -4) },
+          new Impact[] { new Impact(3, 9), new Impact(12, -4), new Impact(20, -1) }
+        },
+        new int[] {
+            150,
+            960
+        });
+    assertEquals(
+        new Impact[][] {
+          new Impact[] { new Impact(2, 10), new Impact(8, -4) },
+          new Impact[] { new Impact(3, 10), new Impact(8, -4), new Impact(12, -3) }
+        },
+        new int[] {
+            110,
+            945
+        },
+        mergedImpacts.getImpacts());
+  }
+
+  private static void assertEquals(Impact[][] impacts, int[] docIdUpTo, Impacts actual) {
+    assertEquals(impacts.length, actual.numLevels());
+    for (int i = 0; i < impacts.length; ++i) {
+      assertEquals(docIdUpTo[i], actual.getDocIdUpTo(i));
+      assertEquals(Arrays.asList(impacts[i]), actual.getImpacts(i));
+    }
+  }
+
+  private static class DummyImpactsEnum extends ImpactsEnum {
+
+    private final long cost;
+    private Impact[][] impacts;
+    private int[] docIdUpTo;
+
+    DummyImpactsEnum(long cost) {
+      this.cost = cost;
+    }
+
+    void reset(Impact[][] impacts, int[] docIdUpTo) {
+      this.impacts = impacts;
+      this.docIdUpTo = docIdUpTo;
+    }
+
+    @Override
+    public void advanceShallow(int target) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Impacts getImpacts() throws IOException {
+      return new Impacts() {
+
+        @Override
+        public int numLevels() {
+          return impacts.length;
+        }
+
+        @Override
+        public int getDocIdUpTo(int level) {
+          return docIdUpTo[level];
+        }
+
+        @Override
+        public List<Impact> getImpacts(int level) {
+          return Arrays.asList(impacts[level]);
+        }
+
+      };
+    }
+
+    @Override
+    public int freq() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int nextPosition() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int startOffset() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int endOffset() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BytesRef getPayload() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int docID() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int nextDoc() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int advance(int target) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long cost() {
+      return cost;
+    }
+
+  }
+
+  public void testRandomTopDocs() throws IOException {
+    Directory dir = newDirectory();
+    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig());
+    int numDocs = atLeast(128 * 8 * 8 * 3); // make sure some terms have skip data
+    for (int i = 0; i < numDocs; ++i) {
+      Document doc = new Document();
+      int numTerms = random().nextInt(1 << random().nextInt(5));
+      String text = IntStream.range(0, numTerms)
+          .mapToObj(index -> random().nextBoolean() ? "a" : random().nextBoolean() ? "b" : "c")
+          .collect(Collectors.joining(" "));
+      doc.add(new TextField("foo", text, Store.NO));
+      w.addDocument(doc);
+    }
+    IndexReader reader = DirectoryReader.open(w);
+    w.close();
+    IndexSearcher searcher = newSearcher(reader);
+
+    for (String firstTerm : new String[] {"a", "b", "c"}) {
+      for (String secondTerm : new String[] {"a", "b", "c"}) {
+        Query query = new PhraseQuery("foo", new BytesRef(firstTerm), new BytesRef(secondTerm));
+
+        TopScoreDocCollector collector1 = TopScoreDocCollector.create(10, null, Integer.MAX_VALUE); // COMPLETE
+        TopScoreDocCollector collector2 = TopScoreDocCollector.create(10, null, 10); // TOP_SCORES
+
+        searcher.search(query, collector1);
+        searcher.search(query, collector2);
+        CheckHits.checkEqual(query, collector1.topDocs().scoreDocs, collector2.topDocs().scoreDocs);
+
+        Query filteredQuery = new BooleanQuery.Builder()
+            .add(query, Occur.MUST)
+            .add(new TermQuery(new Term("foo", "b")), Occur.FILTER)
+            .build();
+
+        collector1 = TopScoreDocCollector.create(10, null, Integer.MAX_VALUE); // COMPLETE
+        collector2 = TopScoreDocCollector.create(10, null, 10); // TOP_SCORES
+        searcher.search(filteredQuery, collector1);
+        searcher.search(filteredQuery, collector2);
+        CheckHits.checkEqual(query, collector1.topDocs().scoreDocs, collector2.topDocs().scoreDocs);
+      }
+    }
+    reader.close();
+    dir.close();
+  }
 }
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/intervals/TermIntervalsSource.java b/lucene/queries/src/java/org/apache/lucene/queries/intervals/TermIntervalsSource.java
index 5ec9161..2859fce 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/intervals/TermIntervalsSource.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/intervals/TermIntervalsSource.java
@@ -241,7 +241,7 @@ class TermIntervalsSource extends IntervalsSource {
   /** A guess of
    * the average number of simple operations for the initial seek and buffer refill
    * per document for the positions of a term.
-   * See also {@link Lucene50PostingsReader.BlockPostingsEnum#nextPosition()}.
+   * See also {@link Lucene50PostingsReader.EverythingEnum#nextPosition()}.
    * <p>
    * Aside: Instead of being constant this could depend among others on
    * {@link Lucene50PostingsFormat#BLOCK_SIZE},
@@ -253,7 +253,7 @@ class TermIntervalsSource extends IntervalsSource {
    */
   private static final int TERM_POSNS_SEEK_OPS_PER_DOC = 128;
 
-  /** Number of simple operations in {@link Lucene50PostingsReader.BlockPostingsEnum#nextPosition()}
+  /** Number of simple operations in {@link Lucene50PostingsReader.EverythingEnum#nextPosition()}
    *  when no seek or buffer refill is done.
    */
   private static final int TERM_OPS_PER_POS = 7;