diff --git a/lucene/core/src/java/org/apache/lucene/index/DocValuesPoolingReader.java b/lucene/core/src/java/org/apache/lucene/index/DocValuesPoolingReader.java
new file mode 100644
index 00000000000..52ff9785a39
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/index/DocValuesPoolingReader.java
@@ -0,0 +1,189 @@
+/*
+ * 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.index;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * Caches docValues for the given {@linkplain LeafReader}.
+ * It only necessary when consumer retrieves same docValues many times per
+ * segment. Returned docValues should be iterated forward only.
+ * Caveat: {@link #getContext()} is completely misguiding for this class since 
+ * it looses baseDoc, ord from underneath context.
+ * @lucene.experimental   
+ * */
+public class DocValuesPoolingReader extends FilterLeafReader {
+
+  /** 
+   * Supplier for docValues.
+   * */
+  @FunctionalInterface
+  protected interface DVSupplier<T extends DocIdSetIterator>{
+    /** 
+     * Yields doc values for the given field name.
+     * */
+    T getDocValues(String field) throws IOException;
+  } 
+  
+  private final Map<String, ? super DocIdSetIterator> cache = new HashMap<>();
+  private final LeafReaderContext context;
+
+  /**
+   * Wraps the reader of the given context. Obtaining docBase and so ones from the given context
+   * */
+  public DocValuesPoolingReader(LeafReaderContext leafCtx) {
+    super(leafCtx.reader());
+    context = new LeafReaderContext(leafCtx.parent, this, leafCtx.ordInParent, leafCtx.docBaseInParent, leafCtx.ord, leafCtx.docBase);
+  }
+
+  /**
+   * Provides context with the same values as the the wrapped one.
+   * */
+  public LeafReaderContext getPoolingContext() {
+    return context;
+  }
+  
+  /**
+   * mimics {@link Map#computeIfAbsent(Object, java.util.function.Function)} backed on internal docvalues cache 
+   * */
+  @SuppressWarnings("unchecked")
+  protected <T extends DocIdSetIterator> T computeIfAbsent(String field, DVSupplier<T> supplier) throws IOException {
+    T dv;
+    if ((dv = (T) cache.get(field)) == null) {
+         dv = supplier.getDocValues(field);
+         if (dv!=null) { // if (!dv.getClass().toString().contains("FieldCache")) { it make no sense to cache FieldCache, but it works.
+           cache.put(field, dv);
+         }
+    }
+    return dv;
+  }
+
+  @Override
+  public CacheHelper getReaderCacheHelper() {
+    return null;
+  }
+
+  @Override
+  public CacheHelper getCoreCacheHelper() {
+    return null;
+  }
+
+  @Override
+  public BinaryDocValues getBinaryDocValues(String field) throws IOException {
+    return computeIfAbsent(field, in::getBinaryDocValues);
+  }
+  
+  @Override
+  public NumericDocValues getNumericDocValues(String field) throws IOException {
+    return computeIfAbsent(field, in::getNumericDocValues);
+  }
+  
+  @Override
+  public SortedNumericDocValues getSortedNumericDocValues(String field) throws IOException {
+    return computeIfAbsent(field, in::getSortedNumericDocValues);
+  }
+  
+  public SortedDocValues getSortedDocValues(String field) throws IOException {
+    return computeIfAbsent(field, in::getSortedDocValues);
+  }
+  
+  @Override
+  public SortedSetDocValues getSortedSetDocValues(String field) throws IOException {
+    return computeIfAbsent(field, field1 -> {
+      final SortedSetDocValues sortedSet = in.getSortedSetDocValues(field1);
+      final SortedDocValues singleton = DocValues.unwrapSingleton(sortedSet);
+      if (singleton!=null) {
+        return new SortedSetDocValues() {
+          private long ord;
+          
+          @Override
+          public int docID() {
+            return singleton.docID();
+          }
+
+          @Override
+          public long nextOrd() {
+            long v = ord;
+            ord = NO_MORE_ORDS;
+            return v;
+          }
+
+          @Override
+          public int nextDoc() throws IOException {
+            int docID = singleton.nextDoc();
+            if (docID != NO_MORE_DOCS) {
+              ord = singleton.ordValue();
+            }
+            return docID;
+          }
+
+          @Override
+          public int advance(int target) throws IOException {
+            int docID = singleton.advance(target);
+            if (docID != NO_MORE_DOCS) {
+              ord = singleton.ordValue();
+            }
+            return docID;
+          }
+
+          @Override
+          public boolean advanceExact(int target) throws IOException {
+            if (singleton.advanceExact(target)) {
+              ord = singleton.ordValue();
+              return true;
+            }
+            return false;
+          }
+
+          @Override
+          public BytesRef lookupOrd(long ord) throws IOException {
+            // cast is ok: single-valued cannot exceed Integer.MAX_VALUE
+            return singleton.lookupOrd((int) ord);
+          }
+
+          @Override
+          public long getValueCount() {
+            return singleton.getValueCount();
+          }
+
+          @Override
+          public long lookupTerm(BytesRef key) throws IOException {
+            return singleton.lookupTerm(key);
+          }
+
+          @Override
+          public TermsEnum termsEnum() throws IOException {
+            return singleton.termsEnum();
+          }
+
+          @Override
+          public long cost() {
+            return singleton.cost();
+          }
+        };
+      } else {
+        return sortedSet;
+      }
+    });
+  }
+  
+}
\ No newline at end of file
diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupHeadsCollector.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupHeadsCollector.java
index f6803858325..6a198c22308 100644
--- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupHeadsCollector.java
+++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupHeadsCollector.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
+import org.apache.lucene.index.DocValuesPoolingReader;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.FieldComparator;
@@ -40,7 +41,7 @@ import org.apache.lucene.util.FixedBitSet;
  *
  * @lucene.experimental
  */
-@SuppressWarnings({"unchecked","rawtypes"})
+@SuppressWarnings({"rawtypes"})
 public abstract class AllGroupHeadsCollector<T> extends SimpleCollector {
 
   private final GroupSelector<T> groupSelector;
@@ -88,7 +89,6 @@ public abstract class AllGroupHeadsCollector<T> extends SimpleCollector {
     for (GroupHead groupHead : groupHeads) {
       bitSet.set(groupHead.doc);
     }
-
     return bitSet;
   }
 
@@ -161,10 +161,13 @@ public abstract class AllGroupHeadsCollector<T> extends SimpleCollector {
 
   @Override
   protected void doSetNextReader(LeafReaderContext context) throws IOException {
-    groupSelector.setNextReader(context);
-    this.context = context;
+    @SuppressWarnings("resource")
+    final LeafReaderContext singleThreadContext = //context;
+                          new DocValuesPoolingReader(context).getPoolingContext();
+    groupSelector.setNextReader(singleThreadContext);
+    this.context = singleThreadContext;
     for (GroupHead<T> head : heads.values()) {
-      head.setNextReader(context);
+      head.setNextReader(singleThreadContext);
     }
   }
 
@@ -203,6 +206,7 @@ public abstract class AllGroupHeadsCollector<T> extends SimpleCollector {
       this.docBase = docBase;
     }
 
+    
     /**
      * Called for each segment
      */
diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/AllGroupHeadsCollectorTest.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/AllGroupHeadsCollectorTest.java
index 2db765abfee..3288f2c74c0 100644
--- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/AllGroupHeadsCollectorTest.java
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/AllGroupHeadsCollectorTest.java
@@ -27,6 +27,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 import org.apache.lucene.analysis.MockAnalyzer;
 import org.apache.lucene.document.BinaryDocValuesField;
@@ -34,6 +35,7 @@ import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.document.NumericDocValuesField;
 import org.apache.lucene.document.SortedDocValuesField;
+import org.apache.lucene.document.SortedSetDocValuesField;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.index.IndexReader;
@@ -48,6 +50,7 @@ import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.SortedSetSortField;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.Bits;
@@ -58,6 +61,7 @@ import org.apache.lucene.util.TestUtil;
 
 public class AllGroupHeadsCollectorTest extends LuceneTestCase {
 
+  @SuppressWarnings("unchecked")
   public void testBasic() throws Exception {
     final String groupField = "author";
     Directory dir = newDirectory();
@@ -73,6 +77,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "random text", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 1));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("1")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("10")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("11")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("1")));
     w.addDocument(doc);
 
     // 1
@@ -81,6 +88,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "some more random text blob", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 2));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("2")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("20")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("21")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("2")));
     w.addDocument(doc);
 
     // 2
@@ -89,6 +99,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "some more random textual data", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 3));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("3")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("30")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("31")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("3")));
     w.addDocument(doc);
     w.commit(); // To ensure a second segment
 
@@ -98,6 +111,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "some random text", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 4));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("4")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("40")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("41")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("4")));
     w.addDocument(doc);
 
     // 4
@@ -106,6 +122,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "some more random text", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 5));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("5")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("50")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("51")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("5")));
     w.addDocument(doc);
 
     // 5
@@ -114,6 +133,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "random blob", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 6));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("6")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("60")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("61")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("6")));
     w.addDocument(doc);
 
     // 6 -- no author field
@@ -121,6 +143,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "random word stuck in alot of other text", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 6));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("6")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("60")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("61")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("6")));
     w.addDocument(doc);
 
     // 7 -- no author field
@@ -128,6 +153,9 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     doc.add(newTextField("content", "random word stuck in alot of other text", Field.Store.NO));
     doc.add(new NumericDocValuesField("id_1", 7));
     doc.add(new SortedDocValuesField("id_2", new BytesRef("7")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("70")));
+    doc.add(new SortedSetDocValuesField("id_3", new BytesRef("71")));
+    doc.add(new SortedSetDocValuesField("id_4", new BytesRef("7")));
     w.addDocument(doc);
 
     IndexReader reader = w.getReader();
@@ -153,19 +181,25 @@ public class AllGroupHeadsCollectorTest extends LuceneTestCase {
     assertTrue(openBitSetContains(new int[]{1, 5}, allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
 
     // STRING sort type triggers different implementation
-    Sort sortWithinGroup2 = new Sort(new SortField("id_2", SortField.Type.STRING, true));
-    allGroupHeadsCollector = createRandomCollector(groupField, sortWithinGroup2);
-    indexSearcher.search(new TermQuery(new Term("content", "random")), allGroupHeadsCollector);
-    assertTrue(arrayContains(new int[]{2, 3, 5, 7}, allGroupHeadsCollector.retrieveGroupHeads()));
-    assertTrue(openBitSetContains(new int[]{2, 3, 5, 7}, allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
-
-    Sort sortWithinGroup3 = new Sort(new SortField("id_2", SortField.Type.STRING, false));
-    allGroupHeadsCollector = createRandomCollector(groupField, sortWithinGroup3);
-    indexSearcher.search(new TermQuery(new Term("content", "random")), allGroupHeadsCollector);
-    // 7 b/c higher doc id wins, even if order of field is in not in reverse.
-    assertTrue(arrayContains(new int[]{0, 3, 4, 6}, allGroupHeadsCollector.retrieveGroupHeads()));
-    assertTrue(openBitSetContains(new int[]{0, 3, 4, 6}, allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
-
+    for (Function<Boolean,SortField> sortFunc : Arrays.<Function<Boolean,SortField>>asList(
+        (r) -> new SortField("id_2", SortField.Type.STRING, (boolean) r), // singular
+        (r) -> new SortedSetSortField("id_3", (boolean) r), //  multivalue 
+        (r) -> new SortedSetSortField("id_4", (boolean) r)  // singular multivalue
+    )) {
+
+      Sort sortWithinGroup2 = new Sort(sortFunc.apply(true));
+      allGroupHeadsCollector = createRandomCollector(groupField, sortWithinGroup2);
+      indexSearcher.search(new TermQuery(new Term("content", "random")), allGroupHeadsCollector);
+      assertTrue(arrayContains(new int[] {2, 3, 5, 7}, allGroupHeadsCollector.retrieveGroupHeads()));
+      assertTrue(openBitSetContains(new int[] {2, 3, 5, 7}, allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
+
+      Sort sortWithinGroup3 = new Sort(sortFunc.apply(false));
+      allGroupHeadsCollector = createRandomCollector(groupField, sortWithinGroup3);
+      indexSearcher.search(new TermQuery(new Term("content", "random")), allGroupHeadsCollector);
+      // 7 b/c higher doc id wins, even if order of field is in not in reverse.
+      assertTrue(arrayContains(new int[] {0, 3, 4, 6}, allGroupHeadsCollector.retrieveGroupHeads()));
+      assertTrue(openBitSetContains(new int[] {0, 3, 4, 6}, allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
+    }
     indexSearcher.getIndexReader().close();
     dir.close();
   }
diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/DocValuesPoolingReaderTest.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/DocValuesPoolingReaderTest.java
new file mode 100644
index 00000000000..25d58b87b95
--- /dev/null
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/DocValuesPoolingReaderTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.grouping;
+
+import java.io.IOException;
+
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.document.BinaryDocValuesField;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.document.SortedDocValuesField;
+import org.apache.lucene.document.SortedNumericDocValuesField;
+import org.apache.lucene.document.SortedSetDocValuesField;
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.DocValuesPoolingReader;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.index.SortedNumericDocValues;
+import org.apache.lucene.index.SortedSetDocValues;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.LuceneTestCase;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class DocValuesPoolingReaderTest extends LuceneTestCase {
+  
+  private static RandomIndexWriter w;
+  private static Directory dir;
+  private static DirectoryReader reader;
+
+  @BeforeClass
+  public static void index() throws IOException {
+    dir = newDirectory();
+    w = new RandomIndexWriter(
+        random(),
+        dir,
+        newIndexWriterConfig(new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy()));
+    for (int i=0; i<2; i++) {
+      Document doc = new Document();
+      doc.add(new BinaryDocValuesField("bin", new BytesRef("binary")));
+      doc.add(new BinaryDocValuesField("bin2", new BytesRef("binary2")));
+      
+      doc.add(new NumericDocValuesField("num", 1L));
+      doc.add(new NumericDocValuesField("num2", 2L));
+      
+      doc.add(new SortedNumericDocValuesField("sortnum", 3L));
+      doc.add(new SortedNumericDocValuesField("sortnum2", 4L));
+      
+      doc.add(new SortedDocValuesField("sort",  new BytesRef("sorted")));
+      doc.add(new SortedDocValuesField("sort2",  new BytesRef("sorted2")));
+      
+      doc.add(new SortedSetDocValuesField("sortset", new BytesRef("sortedset")));
+      doc.add(new SortedSetDocValuesField("sortset2", new BytesRef("sortedset2")));
+      
+      w.addDocument(doc);
+      w.commit();
+    }
+    reader = w.getReader();
+    w.close();
+  }
+  
+  public void testDVCache() throws IOException {
+    assertFalse(reader.leaves().isEmpty());
+    int baseDoc=0;
+    for (LeafReaderContext leaf : reader.leaves()) {
+      assertEquals(baseDoc, leaf.docBase);
+      final DocValuesPoolingReader caching = new DocValuesPoolingReader(leaf);
+      
+      assertSame(assertBinaryDV(caching, "bin", "binary"), 
+          caching.getBinaryDocValues("bin"));
+      assertSame(assertBinaryDV(caching, "bin2", "binary2"), 
+          caching.getBinaryDocValues("bin2"));
+      
+      assertSame(assertNumericDV(caching, "num", 1L), 
+          caching.getNumericDocValues("num"));
+      assertSame(assertNumericDV(caching, "num2", 2L), 
+          caching.getNumericDocValues("num2"));
+      
+      assertSame(assertSortedNumericDV(caching, "sortnum", 3L), 
+          caching.getSortedNumericDocValues("sortnum"));
+      assertSame(assertSortedNumericDV(caching, "sortnum2", 4L), 
+          caching.getSortedNumericDocValues("sortnum2"));
+      
+      assertSame(assertSortedDV(caching, "sort", "sorted"), 
+          caching.getSortedDocValues("sort"));
+      assertSame(assertSortedDV(caching, "sort2", "sorted2"), 
+          caching.getSortedDocValues("sort2"));
+
+      assertSame(assertSortedSetDV(caching, "sortset", "sortedset"), 
+          caching.getSortedSetDocValues("sortset"));
+      assertSame(assertSortedSetDV(caching, "sortset2", "sortedset2"), 
+          caching.getSortedSetDocValues("sortset2"));
+      baseDoc+=leaf.reader().maxDoc();
+    }
+  }
+
+  static BinaryDocValues assertBinaryDV(LeafReader reader, String field, String val) throws IOException {
+    final BinaryDocValues binaryDocValues = reader.getBinaryDocValues(field);
+    assertEquals(0, binaryDocValues.nextDoc());
+    assertEquals(new BytesRef(val), binaryDocValues.binaryValue());
+    return binaryDocValues;
+  }
+
+  static NumericDocValues assertNumericDV(LeafReader reader, String field, long val) throws IOException {
+    final NumericDocValues numericDocValues = reader.getNumericDocValues(field);
+    assertEquals(0, numericDocValues.nextDoc());
+    assertEquals(val, numericDocValues.longValue());
+    return numericDocValues;
+  }
+
+  static SortedNumericDocValues assertSortedNumericDV(LeafReader reader, String field, long val) throws IOException {
+    final SortedNumericDocValues numericDocValues = reader.getSortedNumericDocValues(field);
+    assertEquals(0, numericDocValues.nextDoc());
+    assertEquals(val, numericDocValues.nextValue());
+    return numericDocValues;
+  }
+  
+  static SortedDocValues assertSortedDV(LeafReader reader, String field, String val) throws IOException {
+    final SortedDocValues sortedDocValues = reader.getSortedDocValues(field);
+    assertEquals(0, sortedDocValues.nextDoc());
+    assertEquals(new BytesRef(val), sortedDocValues.lookupOrd(sortedDocValues.ordValue()));
+    return sortedDocValues;
+  }
+  
+  static SortedSetDocValues assertSortedSetDV(LeafReader reader, String field, String val) throws IOException {
+    final SortedSetDocValues sortedDocValues = reader.getSortedSetDocValues(field);
+    assertEquals(0, sortedDocValues.nextDoc());
+    assertEquals(new BytesRef(val), sortedDocValues.lookupOrd(sortedDocValues.nextOrd()));
+    return sortedDocValues;
+  }  
+
+  @AfterClass
+  public static void sweep() throws IOException {
+    reader.close();
+    dir.close();
+  }
+}
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java b/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
index 79c35b89fb9..d1473ec1ecc 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
@@ -112,9 +112,9 @@ public class BlockJoinSelector {
    *  one value per parent among its {@code children} using the configured
    *  {@code selection} type. */
   public static SortedDocValues wrap(final SortedDocValues values, Type selection, BitSet parents, DocIdSetIterator children) {
-    if (values.docID() != -1) {
-      throw new IllegalArgumentException("values iterator was already consumed: values.docID=" + values.docID());
-    }
+    //if (values.docID() != -1) {
+    //  throw new IllegalArgumentException("values iterator was already consumed: values.docID=" + values.docID());
+    //}
     return ToParentDocValues.wrap(values, selection, parents, children);
   }
 
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java b/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java
index 4e227edbab3..a24386fae4d 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java
@@ -17,11 +17,9 @@
 package org.apache.lucene.search.join;
 
 import java.io.IOException;
-import java.util.Arrays;
 
 import org.apache.lucene.index.NumericDocValues;
 import org.apache.lucene.index.SortedDocValues;
-import org.apache.lucene.search.ConjunctionDISI;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.join.BlockJoinSelector.Type;
 import org.apache.lucene.util.BitSet;
@@ -170,16 +168,19 @@ class ToParentDocValues extends DocIdSetIterator {
 
   private ToParentDocValues(DocIdSetIterator values, BitSet parents, DocIdSetIterator children, Accumulator collect) {
     this.parents = parents;
-    childWithValues = ConjunctionDISI.intersectIterators(Arrays.asList(children, values));
+    //childWithValues = ConjunctionDISI.intersectIterators(Arrays.asList(children, values));
+    this.children = children;
+    this.childValues = values;
     this.collector = collect;
   }
 
   
   final private BitSet parents;
-  private int docID = -1;
   final private Accumulator collector;
-  boolean seen=false;
-  private DocIdSetIterator childWithValues;
+  private int docID = -1;
+  private boolean seen=false;
+  final private DocIdSetIterator childValues;
+  final private DocIdSetIterator children;
 
   @Override
   public int docID() {
@@ -189,30 +190,43 @@ class ToParentDocValues extends DocIdSetIterator {
   @Override
   public int nextDoc() throws IOException {
     assert docID != NO_MORE_DOCS;
-    
-    assert childWithValues.docID()!=docID || docID==-1;
-    if (childWithValues.docID()<docID || docID==-1) {
-      childWithValues.nextDoc();
+    // prev advanceExact might move it into mid-block - at child,
+    // that's weird, let's drag to next parent first
+    if (docID!=-1) {
+      docID=parents.nextSetBit(docID);
+    }
+    // ^ is noop if it's correctly set at parent.
+    if (docID == NO_MORE_DOCS) {
+      return NO_MORE_DOCS;
+    }
+    assert children.docID()!=docID || docID==-1;
+    while ((children.docID() != NO_MORE_DOCS) && (children.docID()<docID || docID==-1 || !advanceExactChildDV(children.docID()))) {
+      children.nextDoc();
+      if (children.docID() == NO_MORE_DOCS || advanceExactChildDV(children.docID())) {
+        break;
+      }
     }
-    if (childWithValues.docID() == NO_MORE_DOCS) {
+    if (children.docID() == NO_MORE_DOCS) {
       docID = NO_MORE_DOCS;
       return docID;
     }
     
 
-    assert parents.get(childWithValues.docID()) == false;
+    assert parents.get(children.docID()) == false;
     
-    int nextParentDocID = parents.nextSetBit(childWithValues.docID());
+    int nextParentDocID = parents.nextSetBit(children.docID());
     collector.reset();
     seen=true;
     
     while (true) {
-      int childDocID = childWithValues.nextDoc();
+      int childDocID = children.nextDoc();
       assert childDocID != nextParentDocID;
       if (childDocID > nextParentDocID) {
         break;
       }
-      collector.increment();
+      if (advanceExactChildDV(childDocID)) {
+        collector.increment();
+      }
     }
 
     docID = nextParentDocID;
@@ -231,8 +245,8 @@ class ToParentDocValues extends DocIdSetIterator {
       return nextDoc();
     }
     int prevParentDocID = parents.prevSetBit(target-1);
-    if (childWithValues.docID() <= prevParentDocID) {
-      childWithValues.advance(prevParentDocID+1);
+    if (children.docID() <= prevParentDocID) {
+      children.advance(prevParentDocID+1);
     }
     return nextDoc();
   }
@@ -245,39 +259,51 @@ class ToParentDocValues extends DocIdSetIterator {
     int previousDocId = docID;
     docID = targetParentDocID;
     if (targetParentDocID == previousDocId) {
-      return seen;//ord != -1; rlly???
+      return seen;
     }
-    docID = targetParentDocID;
-    seen =false;
-    //ord = -1;
+    seen = false;
+
     if (parents.get(targetParentDocID) == false) {
       return false;
     }
     int prevParentDocId = docID == 0 ? -1 : parents.prevSetBit(docID - 1);
-    int childDoc = childWithValues.docID();
+    int childDoc = children.docID();
     if (childDoc <= prevParentDocId) {
-      childDoc = childWithValues.advance(prevParentDocId + 1);
-    }
-    if (childDoc >= docID) {
-      return false;
+      childDoc = children.advance(prevParentDocId + 1);
     }
     
-    if (childWithValues.docID() < docID) {
-      collector.reset();
-      seen=true;
-      childWithValues.nextDoc();
+    for(;children.docID() < docID && !seen; children.nextDoc()) {
+      if (advanceExactChildDV(children.docID())) {
+        collector.reset();
+        seen=true;
+      } 
     }
     
     if (seen == false) {
       return false;
     }
 
-    for (int doc = childWithValues.docID(); doc < docID; doc = childWithValues.nextDoc()) {
+    for (int doc = children.docID(); doc < docID; doc = children.nextDoc()) {
+      if (advanceExactChildDV(doc)) {
         collector.increment();
+      }
     }
     return true;
   }
 
+  private boolean advanceExactChildDV(int docnum) throws IOException {
+    if (childValues instanceof SortedDocValues) {
+      return ((SortedDocValues) childValues).advanceExact(docnum);
+    } else {
+      if (childValues instanceof NumericDocValues) {
+        return ((NumericDocValues) childValues).advanceExact(docnum);
+      } else {
+        throw new UnsupportedOperationException("unexpected " + childValues 
+            + " for advanceExact()");
+      }
+    }
+  }
+
   @Override
   public long cost() {
     return 0;
diff --git a/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/IntFieldSource.java b/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/IntFieldSource.java
index 8d2ab7fe965..d5a68924aa1 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/IntFieldSource.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/IntFieldSource.java
@@ -55,27 +55,10 @@ public class IntFieldSource extends FieldCacheSource {
     final NumericDocValues arr = getNumericDocValues(context, readerContext);
 
     return new IntDocValues(this) {
-      int lastDocID;
-
-      private int getValueForDoc(int doc) throws IOException {
-        if (doc < lastDocID) {
-          throw new IllegalArgumentException("docs were sent out-of-order: lastDocID=" + lastDocID + " vs docID=" + doc);
-        }
-        lastDocID = doc;
-        int curDocID = arr.docID();
-        if (doc > curDocID) {
-          curDocID = arr.advance(doc);
-        }
-        if (doc == curDocID) {
-          return (int) arr.longValue();
-        } else {
-          return 0;
-        }
-      }
 
       @Override
       public int intVal(int doc) throws IOException {
-        return getValueForDoc(doc);
+        return arr.advanceExact(doc) ? (int)arr.longValue() : 0;
       }
 
       @Override
@@ -85,8 +68,7 @@ public class IntFieldSource extends FieldCacheSource {
 
       @Override
       public boolean exists(int doc) throws IOException {
-        getValueForDoc(doc);
-        return arr.docID() == doc;
+        return arr.advanceExact(doc);
       }
 
       @Override
@@ -101,8 +83,8 @@ public class IntFieldSource extends FieldCacheSource {
 
           @Override
           public void fillValue(int doc) throws IOException {
-            mval.value = getValueForDoc(doc);
-            mval.exists = arr.docID() == doc;
+            mval.exists = arr.advanceExact(doc);
+            mval.value = mval.exists ? (int)arr.longValue() : 0;
           }
         };
       }
diff --git a/lucene/test-framework/src/java/org/apache/lucene/codecs/asserting/AssertingDocValuesFormat.java b/lucene/test-framework/src/java/org/apache/lucene/codecs/asserting/AssertingDocValuesFormat.java
index fd8c246ed5c..340eb65e4e1 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/codecs/asserting/AssertingDocValuesFormat.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/codecs/asserting/AssertingDocValuesFormat.java
@@ -24,6 +24,7 @@ import org.apache.lucene.codecs.DocValuesFormat;
 import org.apache.lucene.codecs.DocValuesProducer;
 import org.apache.lucene.index.AssertingLeafReader;
 import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.index.FieldInfo;
 import org.apache.lucene.index.NumericDocValues;
@@ -285,7 +286,9 @@ public class AssertingDocValuesFormat extends DocValuesFormat {
       assert field.getDocValuesType() == DocValuesType.SORTED_SET;
       SortedSetDocValues values = in.getSortedSet(field);
       assert values != null;
-      return new AssertingLeafReader.AssertingSortedSetDocValues(values, maxDoc);
+      final SortedDocValues singleton = DocValues.unwrapSingleton(values);
+      return singleton==null ? new AssertingLeafReader.AssertingSortedSetDocValues(values, maxDoc) :
+        DocValues.singleton(new AssertingLeafReader.AssertingSortedDocValues(singleton, maxDoc));
     }
     
     @Override
diff --git a/solr/core/src/java/org/apache/solr/schema/BoolField.java b/solr/core/src/java/org/apache/solr/schema/BoolField.java
index d9f9f0e5318..65eff997a73 100644
--- a/solr/core/src/java/org/apache/solr/schema/BoolField.java
+++ b/solr/core/src/java/org/apache/solr/schema/BoolField.java
@@ -250,25 +250,14 @@ public class BoolField extends PrimitiveFieldType {
 
       return new BoolDocValues(this) {
 
-        private int getOrdForDoc(int doc) throws IOException {
-          if (doc > sindex.docID()) {
-            sindex.advance(doc);
-          }
-          if (doc == sindex.docID()) {
-            return sindex.ordValue();
-          } else {
-            return -1;
-          }
-        }
-
         @Override
         public boolean boolVal(int doc) throws IOException {
-          return getOrdForDoc(doc) == trueOrd;
+          return sindex.advanceExact(doc) && sindex.ordValue() == trueOrd;
         }
 
         @Override
         public boolean exists(int doc) throws IOException {
-          return getOrdForDoc(doc) != -1;
+          return sindex.advanceExact(doc);
         }
 
         @Override
@@ -283,9 +272,7 @@ public class BoolField extends PrimitiveFieldType {
 
             @Override
             public void fillValue(int doc) throws IOException {
-              int ord = getOrdForDoc(doc);
-              mval.value = (ord == trueOrd);
-              mval.exists = (ord != -1);
+              mval.value = (mval.exists = sindex.advanceExact(doc)) && sindex.ordValue() == trueOrd;
             }
           };
         }
diff --git a/solr/core/src/test/org/apache/solr/search/grouping/AllGroupHeadsCollectorTest.java b/solr/core/src/test/org/apache/solr/search/grouping/AllGroupHeadsCollectorTest.java
new file mode 100644
index 00000000000..2427ff3bc51
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/grouping/AllGroupHeadsCollectorTest.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search.grouping;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.document.BinaryDocValuesField;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.document.SortedDocValuesField;
+import org.apache.lucene.document.SortedSetDocValuesField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.DocValuesType;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.queries.function.valuesource.BytesRefFieldSource;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField.Type;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.grouping.AllGroupHeadsCollector;
+import org.apache.lucene.search.grouping.TermGroupSelector;
+import org.apache.lucene.search.grouping.ValueSourceGroupSelector;
+import org.apache.lucene.search.join.QueryBitSetProducer;
+import org.apache.lucene.search.join.ToParentBlockJoinSortField;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.solr.SolrTestCase;
+
+public class AllGroupHeadsCollectorTest extends SolrTestCase {
+
+  public void testBasicBlockJoin() throws Exception {
+    final String groupField = "author";
+    Directory dir = newDirectory();
+    RandomIndexWriter w = new RandomIndexWriter(
+        random(),
+        dir,
+        newIndexWriterConfig(new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy()));
+    DocValuesType valueType = DocValuesType.SORTED;
+
+    // 0
+    Document doc = new Document();
+    addGroupField(doc, groupField, "author1", valueType);
+    doc.add(newTextField("content", "random text", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 1));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("1")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("10")),
+                    new SortedSetDocValuesField("id_3", new BytesRef("11")));
+
+    // 1
+    doc = new Document();
+    addGroupField(doc, groupField, "author1", valueType);
+    doc.add(newTextField("content", "some more random text blob", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 2));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("2")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("20")),
+          new SortedSetDocValuesField("id_3", new BytesRef("21")));
+
+    // 2
+    doc = new Document();
+    addGroupField(doc, groupField, "author1", valueType);
+    doc.add(newTextField("content", "some more random textual data", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 3));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("3")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("30")),
+      new SortedSetDocValuesField("id_3", new BytesRef("31")));
+    w.commit(); // To ensure a second segment
+
+    // 3
+    doc = new Document();
+    addGroupField(doc, groupField, "author2", valueType);
+    doc.add(newTextField("content", "some random text", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 4));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("4")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("40")),
+         new SortedSetDocValuesField("id_3", new BytesRef("41")));
+
+    // 4
+    doc = new Document();
+    addGroupField(doc, groupField, "author3", valueType);
+    doc.add(newTextField("content", "some more random text", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 5));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("5")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("50")),
+                 new SortedSetDocValuesField("id_3", new BytesRef("51")));
+
+    // 5
+    doc = new Document();
+    addGroupField(doc, groupField, "author3", valueType);
+    doc.add(newTextField("content", "random blob", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 6));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("6")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("60")),
+              new SortedSetDocValuesField("id_3", new BytesRef("61")));
+
+    // 6 -- no author field
+    doc = new Document();
+    doc.add(newTextField("content", "random word stuck in alot of other text", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 6));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("6")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("60")),
+            new SortedSetDocValuesField("id_3", new BytesRef("61")));
+
+    // 7 -- no author field
+    doc = new Document();
+    doc.add(newTextField("content", "random word stuck in alot of other text", Field.Store.NO));
+    doc.add(new NumericDocValuesField("id_1", 7));
+    doc.add(new SortedDocValuesField("id_2", new BytesRef("7")));
+    addParent(w, doc, new SortedSetDocValuesField("id_3", new BytesRef("70")),
+           new SortedSetDocValuesField("id_3", new BytesRef("71")));
+
+    IndexReader reader = w.getReader();
+    IndexSearcher indexSearcher = newSearcher(reader);
+
+    w.close();
+    int maxDoc = reader.maxDoc();
+
+    final QueryBitSetProducer parentFilter = new QueryBitSetProducer(new TermQuery(new Term("type","parent")));
+    final QueryBitSetProducer childFilter = new QueryBitSetProducer(new TermQuery(new Term("type","child")));
+    Sort sortWithinGroup2 = new Sort(new ToParentBlockJoinSortField("id_3", Type.STRING, true, 
+        parentFilter, childFilter));
+    AllGroupHeadsCollector<?> allGroupHeadsCollector = createRandomCollector(groupField, sortWithinGroup2);
+    indexSearcher.search(new TermQuery(new Term("content", "random")), allGroupHeadsCollector);
+    assertTrue(arrayContains(shiftForBlocks(2, 3, 5, 7), allGroupHeadsCollector.retrieveGroupHeads()));
+    assertTrue(openBitSetContains(shiftForBlocks(2, 3, 5, 7), allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
+
+    Sort sortWithinGroup3 = new Sort(new ToParentBlockJoinSortField("id_3", Type.STRING, false, 
+        parentFilter, childFilter));
+    allGroupHeadsCollector = createRandomCollector(groupField, sortWithinGroup3);
+    indexSearcher.search(new TermQuery(new Term("content", "random")), allGroupHeadsCollector);
+    // 7 b/c higher doc id wins, even if order of field is in not in reverse.
+    assertTrue(arrayContains(shiftForBlocks(0, 3, 4, 6), allGroupHeadsCollector.retrieveGroupHeads()));
+    assertTrue(openBitSetContains(shiftForBlocks(0, 3, 4, 6), allGroupHeadsCollector.retrieveGroupHeads(maxDoc), maxDoc));
+    indexSearcher.getIndexReader().close();
+    dir.close();
+  }
+
+  /** converts flat docnums from lucene/grouping AllGroupHeadsCollectorTest.testBasic() to parent docNums (zero based) with two child docs */
+  private int[] shiftForBlocks(int ... plainDocNums) {
+    return Arrays.stream(plainDocNums).map((docNum)-> docNum*3+2).toArray();
+  }
+
+  private void addParent(RandomIndexWriter w, Document parentDoc, Field ... childFields) throws IOException {
+    final List<Iterable<? extends IndexableField>> block = new ArrayList<>();
+    for (Field childField : childFields) {
+      final Document child = new Document();
+      child.add(childField);
+      child.add(new StringField("type", "child", Store.NO));
+      block.add(child);
+    }
+    parentDoc.add(new StringField("type", "parent", Store.NO));
+    block.add(parentDoc);
+    w.addDocuments(block);
+  }
+
+  private boolean arrayContains(int[] expected, int[] actual) {
+    Arrays.sort(actual); // in some cases the actual docs aren't sorted by docid. This method expects that.
+    if (expected.length != actual.length) {
+      return false;
+    }
+
+    for (int e : expected) {
+      boolean found = false;
+      for (int a : actual) {
+        if (e == a) {
+          found = true;
+          break;
+        }
+      }
+
+      if (!found) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  private boolean openBitSetContains(int[] expectedDocs, Bits actual, int maxDoc) throws IOException {
+    assert actual instanceof FixedBitSet;
+    if (expectedDocs.length != ((FixedBitSet)actual).cardinality()) {
+      return false;
+    }
+
+    FixedBitSet expected = new FixedBitSet(maxDoc);
+    for (int expectedDoc : expectedDocs) {
+      expected.set(expectedDoc);
+    }
+
+    for (int docId = expected.nextSetBit(0); docId != DocIdSetIterator.NO_MORE_DOCS; docId = docId + 1 >= expected.length() ? DocIdSetIterator.NO_MORE_DOCS : expected.nextSetBit(docId + 1)) {
+      if (!actual.get(docId)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  @SuppressWarnings({"unchecked","rawtypes"})
+  private AllGroupHeadsCollector<?> createRandomCollector(String groupField, Sort sortWithinGroup) {
+    if (random().nextBoolean()) {
+      ValueSource vs = new BytesRefFieldSource(groupField);
+      return AllGroupHeadsCollector.newCollector(new ValueSourceGroupSelector(vs, new HashMap<>()), sortWithinGroup);
+    } else {
+      return AllGroupHeadsCollector.newCollector(new TermGroupSelector(groupField), sortWithinGroup);
+    }
+  }
+
+  private void addGroupField(Document doc, String groupField, String value, DocValuesType valueType) {
+    Field valuesField = null;
+    switch(valueType) {
+      case BINARY:
+        valuesField = new BinaryDocValuesField(groupField, new BytesRef(value));
+        break;
+      case SORTED:
+        valuesField = new SortedDocValuesField(groupField, new BytesRef(value));
+        break;
+      default:
+        fail("unhandled type");
+    }
+    doc.add(valuesField);
+  }
+}
