From 0cde4babc3d4ab7c2766fea3baf131d9fbc3372f Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Wed, 10 Jun 2026 22:20:06 +0800 Subject: [PATCH 1/7] Introduce CachingCollectorManager to parallelize search when using CachingCollector Signed-off-by: Binlong Gao --- lucene/CHANGES.txt | 2 + .../search/CachingCollectorManager.java | 130 +++++++++++++++++ .../search/grouping/GroupingSearch.java | 131 ++++++++++++------ .../search/grouping/TestGroupingSearch.java | 13 +- 4 files changed, 227 insertions(+), 49 deletions(-) create mode 100644 lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 978f361159f3..8e2ab82c6241 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -384,6 +384,8 @@ Improvements * GITHUB#15660: Introduce LargeNumHitsTopDocsCollectorManager to parallelize search when using LargeNumHitsTopDocsCollector. (Binlong Gao) +* GITHUB#15660: Introduce CachingCollectorManager to parallelize search when using CachingCollector. (Binlong Gao) + * GITHUB#16088: SearchGroup#merge now returns an empty collection instead of null when topGroups is empty. (Luca Cavanna) * GITHUB#16144: Clarify LeafCollector batch collection scoring contract. (Costin Leau) diff --git a/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java b/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java new file mode 100644 index 000000000000..f51b51ec6a10 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.search; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A {@link CollectorManager} that wraps a delegate {@link CollectorManager} and caches all + * collected documents (and optionally scores) per slice, so they can be replayed to a second-pass + * {@link CollectorManager} without re-running the query. + * + *

One {@link CachingCollector} is created per slice. During {@link #replay}, each cached slice + * is replayed into a fresh second-pass collector, and all second-pass collectors are reduced + * together. This works correctly with both sequential and concurrent search. + * + *

Example usage: + * + *

+ * CachingCollectorManager<C1, R1> caching = new CachingCollectorManager<>(
+ *     firstPassManager, cacheScores, maxRAMMB);
+ * R1 firstResult = searcher.search(query, caching);
+ *
+ * if (caching.isCached()) {
+ *   R2 secondResult = caching.replay(secondPassManager);
+ * } else {
+ *   // cache overflowed — re-run the query
+ *   R2 secondResult = searcher.search(query, secondPassManager);
+ * }
+ * 
+ * + * @lucene.experimental + */ +public class CachingCollectorManager implements CollectorManager { + + private final CollectorManager delegate; + private final boolean cacheScores; + private final Double maxRAMMB; + private final Integer maxDocsToCache; + + // One CachingCollector per slice, thread-safe for concurrent newCollector() calls. + private final List cachingCollectors = new CopyOnWriteArrayList<>(); + // The original unwrapped collectors + private final List originalCollectors = new CopyOnWriteArrayList<>(); + + /** + * @param delegate the first-pass {@link CollectorManager} + * @param cacheScores whether to cache scores in addition to document IDs + * @param maxRAMMB the maximum RAM in MB to use per slice cache, or null if using maxDocsToCache + * @param maxDocsToCache the maximum number of documents to cache per slice, or null if using + * maxRAMMB + */ + public CachingCollectorManager( + CollectorManager delegate, + boolean cacheScores, + Double maxRAMMB, + Integer maxDocsToCache) { + if (maxRAMMB == null && maxDocsToCache == null) { + throw new IllegalArgumentException("Either maxRAMMB or maxDocsToCache must be set"); + } + this.delegate = delegate; + this.cacheScores = cacheScores; + this.maxRAMMB = maxRAMMB; + this.maxDocsToCache = maxDocsToCache; + } + + @Override + public C newCollector() throws IOException { + C collector = delegate.newCollector(); + originalCollectors.add(collector); + CachingCollector cache = + maxDocsToCache != null + ? CachingCollector.create(collector, cacheScores, maxDocsToCache) + : CachingCollector.create(collector, cacheScores, maxRAMMB); + cachingCollectors.add(cache); + @SuppressWarnings("unchecked") + C wrapped = (C) cache; + return wrapped; + } + + @Override + public R reduce(Collection collectors) throws IOException { + return delegate.reduce(originalCollectors); + } + + /** + * Returns {@code true} if all per-slice caches are intact (none overflowed their RAM budget), + * meaning {@link #replay} can be called. + */ + public boolean isCached() { + return !cachingCollectors.isEmpty() + && cachingCollectors.stream().allMatch(CachingCollector::isCached); + } + + /** + * Replays each per-slice cache into a fresh second-pass collector, then reduces all results. + * + * @throws IllegalStateException if any slice cache is not available + */ + public R2 replay(CollectorManager secondPassManager) + throws IOException { + if (!isCached()) { + throw new IllegalStateException("cache is not available; re-run the query instead"); + } + List secondCollectors = new ArrayList<>(cachingCollectors.size()); + for (CachingCollector cache : cachingCollectors) { + C2 secondCollector = secondPassManager.newCollector(); + cache.replay(secondCollector); + secondCollectors.add(secondCollector); + } + return secondPassManager.reduce(secondCollectors); + } +} diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java index 91bcbf56da84..5dfc94019efc 100644 --- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java +++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java @@ -17,14 +17,18 @@ package org.apache.lucene.search.grouping; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.apache.lucene.queries.function.ValueSource; -import org.apache.lucene.search.CachingCollector; +import org.apache.lucene.search.CachingCollectorManager; import org.apache.lucene.search.Collector; +import org.apache.lucene.search.CollectorManager; import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.MultiCollector; +import org.apache.lucene.search.MultiCollectorManager; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Sort; @@ -41,7 +45,7 @@ */ public class GroupingSearch { - private final GroupSelector grouper; + private final Supplier> grouperFactory; private final Query groupEndDocs; private Sort groupSort = Sort.RELEVANCE; @@ -69,7 +73,17 @@ public class GroupingSearch { * @param groupField The name of the field to group by. */ public GroupingSearch(String groupField) { - this(new TermGroupSelector(groupField), null); + this(() -> new TermGroupSelector(groupField), null); + } + + /** + * Constructs a GroupingSearch instance that groups documents using a {@link + * GroupSelector} factory. + * + * @param grouperFactory a factory that creates fresh {@link GroupSelector} instances + */ + public GroupingSearch(Supplier> grouperFactory) { + this(grouperFactory, null); } /** @@ -77,9 +91,11 @@ public GroupingSearch(String groupField) { * GroupSelector} * * @param groupSelector a {@link GroupSelector} that defines groups for this GroupingSearch + * @deprecated Use {@link #GroupingSearch(Supplier)} to provide a factory for concurrent search */ + @Deprecated public GroupingSearch(GroupSelector groupSelector) { - this(groupSelector, null); + this(() -> groupSelector, null); } /** @@ -90,7 +106,7 @@ public GroupingSearch(GroupSelector groupSelector) { * @param valueSourceContext The context of the specified groupFunction */ public GroupingSearch(ValueSource groupFunction, Map valueSourceContext) { - this(new ValueSourceGroupSelector(groupFunction, valueSourceContext), null); + this(() -> new ValueSourceGroupSelector(groupFunction, valueSourceContext), null); } /** @@ -103,8 +119,8 @@ public GroupingSearch(Query groupEndDocs) { this(null, groupEndDocs); } - private GroupingSearch(GroupSelector grouper, Query groupEndDocs) { - this.grouper = grouper; + private GroupingSearch(Supplier> grouperFactory, Query groupEndDocs) { + this.grouperFactory = grouperFactory; this.groupEndDocs = groupEndDocs; } @@ -123,7 +139,7 @@ private GroupingSearch(GroupSelector grouper, Query groupEndDocs) { @SuppressWarnings("unchecked") public TopGroups search( IndexSearcher searcher, Query query, int groupOffset, int groupLimit) throws IOException { - if (grouper != null) { + if (grouperFactory != null) { return groupByFieldOrFunction(searcher, query, groupOffset, groupLimit); } else if (groupEndDocs != null) { return (TopGroups) groupByDocBlock(searcher, query, groupOffset, groupLimit); @@ -134,59 +150,82 @@ public TopGroups search( } @SuppressWarnings({"unchecked", "rawtypes"}) - protected TopGroups groupByFieldOrFunction( + protected TopGroups groupByFieldOrFunction( IndexSearcher searcher, Query query, int groupOffset, int groupLimit) throws IOException { - int topN = groupOffset + groupLimit; + @SuppressWarnings("unchecked") + Supplier> typedGrouperFactory = + (Supplier>) (Supplier) grouperFactory; + FirstPassGroupingCollectorManager firstPassManager = + new FirstPassGroupingCollectorManager<>( + typedGrouperFactory, groupSort, groupOffset, groupLimit, ignoreDocsWithoutGroupField); + List> firstRoundManagers = new ArrayList<>(); + firstRoundManagers.add(firstPassManager); + AllGroupsCollectorManager allGroupsManager; + if (allGroups) { + allGroupsManager = new AllGroupsCollectorManager<>(typedGrouperFactory); + firstRoundManagers.add(allGroupsManager); + } - final FirstPassGroupingCollector firstPassCollector = - new FirstPassGroupingCollector(grouper, groupSort, topN, ignoreDocsWithoutGroupField); - final AllGroupsCollector allGroupsCollector = - allGroups ? new AllGroupsCollector(grouper) : null; - final AllGroupHeadsCollector allGroupHeadsCollector = - allGroupHeads ? AllGroupHeadsCollector.newCollector(grouper, sortWithinGroup) : null; + AllGroupHeadsCollectorManager allGroupHeadsManager; + if (allGroupHeads) { + allGroupHeadsManager = + new AllGroupHeadsCollectorManager<>(typedGrouperFactory, sortWithinGroup); + firstRoundManagers.add(allGroupHeadsManager); + } - final Collector firstRound = - MultiCollector.wrap(firstPassCollector, allGroupsCollector, allGroupHeadsCollector); + CollectorManager firstRoundManager = + new MultiCollectorManager(firstRoundManagers.toArray(CollectorManager[]::new)); - CachingCollector cachedCollector = null; + CachingCollectorManager cachingManager = null; + Object[] firstRoundResults; if (maxCacheRAMMB != null || maxDocsToCache != null) { - if (maxCacheRAMMB != null) { - cachedCollector = CachingCollector.create(firstRound, cacheScores, maxCacheRAMMB); - } else { - cachedCollector = CachingCollector.create(firstRound, cacheScores, maxDocsToCache); - } - searcher.search(query, cachedCollector); + cachingManager = + new CachingCollectorManager<>( + firstRoundManager, cacheScores, maxCacheRAMMB, maxDocsToCache); + firstRoundResults = searcher.search(query, cachingManager); } else { - searcher.search(query, firstRound); + firstRoundResults = searcher.search(query, firstRoundManager); } - matchingGroups = allGroups ? allGroupsCollector.getGroups() : Collections.emptyList(); - matchingGroupHeads = - allGroupHeads - ? allGroupHeadsCollector.retrieveGroupHeads(searcher.getIndexReader().maxDoc()) - : new Bits.MatchNoBits(searcher.getIndexReader().maxDoc()); - - Collection topSearchGroups = firstPassCollector.getTopGroups(groupOffset); - if (topSearchGroups == null) { - return new TopGroups(new SortField[0], new SortField[0], 0, 0, new GroupDocs[0], Float.NaN); + int resultIdx = 0; + Collection> topSearchGroups = + (Collection>) firstRoundResults[resultIdx++]; + if (topSearchGroups.isEmpty()) { + return new TopGroups<>(new SortField[0], new SortField[0], 0, 0, new GroupDocs[0], Float.NaN); } - int topNInsideGroup = groupDocsOffset + groupDocsLimit; - TopGroupsCollector secondPassCollector = - new TopGroupsCollector( - grouper, topSearchGroups, groupSort, sortWithinGroup, topNInsideGroup, includeMaxScore); + matchingGroups = + allGroups ? (Collection) firstRoundResults[resultIdx++] : Collections.emptyList(); - if (cachedCollector != null && cachedCollector.isCached()) { - cachedCollector.replay(secondPassCollector); + if (allGroupHeads) { + AllGroupHeadsCollectorManager.GroupHeadsResult headsResult = + (AllGroupHeadsCollectorManager.GroupHeadsResult) firstRoundResults[resultIdx]; + matchingGroupHeads = headsResult.retrieveGroupHeads(searcher.getIndexReader().maxDoc()); + } else { + matchingGroupHeads = new Bits.MatchNoBits(searcher.getIndexReader().maxDoc()); + } + + TopGroupsCollectorManager secondPassManager = + new TopGroupsCollectorManager<>( + typedGrouperFactory, + topSearchGroups, + groupSort, + sortWithinGroup, + groupDocsOffset, + groupDocsLimit, + includeMaxScore); + + TopGroups secondResult; + if (cachingManager != null && cachingManager.isCached()) { + secondResult = cachingManager.replay(secondPassManager); } else { - searcher.search(query, secondPassCollector); + secondResult = searcher.search(query, secondPassManager); } if (allGroups) { - return new TopGroups( - secondPassCollector.getTopGroups(groupDocsOffset), matchingGroups.size()); + return new TopGroups<>(secondResult, matchingGroups.size()); } else { - return secondPassCollector.getTopGroups(groupDocsOffset); + return secondResult; } } diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java index 308b7d2c0702..815007e2b6c2 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java @@ -110,10 +110,17 @@ public void testBasic() throws Exception { // 6 -- no author field doc = new Document(); doc.add(new TextField("content", "random word stuck in alot of other text", Field.Store.YES)); - doc.add(new Field("id", "6", customType)); - doc.add(new StringField("groupend", "x", Field.Store.NO)); + doc.add(new Field("id", "7", customType)); + documents.add(doc); - w.addDocument(doc); + // 7 -- no match document + doc = new Document(); + doc.add(new TextField("content", "no-match", Field.Store.YES)); + doc.add(new Field("id", "8", customType)); + doc.add(new StringField("groupend", "x", Field.Store.NO)); + documents.add(doc); + w.addDocuments(documents); + documents.clear(); IndexSearcher indexSearcher = newSearcher(w.getReader()); indexSearcher.setSimilarity(new BM25Similarity()); From 7bc5217d19542629f324be9109a1eb7488d037ea Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Thu, 11 Jun 2026 22:02:52 +0800 Subject: [PATCH 2/7] Modify change log Signed-off-by: Binlong Gao --- lucene/CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 8e2ab82c6241..f0190cc3e173 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -384,7 +384,7 @@ Improvements * GITHUB#15660: Introduce LargeNumHitsTopDocsCollectorManager to parallelize search when using LargeNumHitsTopDocsCollector. (Binlong Gao) -* GITHUB#15660: Introduce CachingCollectorManager to parallelize search when using CachingCollector. (Binlong Gao) +* GITHUB#16247: Introduce CachingCollectorManager to parallelize search when using CachingCollector. (Binlong Gao) * GITHUB#16088: SearchGroup#merge now returns an empty collection instead of null when topGroups is empty. (Luca Cavanna) From 0466d2faab6534718ee26615f2e60f2fb1fb825d Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Wed, 17 Jun 2026 15:35:28 +0800 Subject: [PATCH 3/7] Optimize code and fix bug Signed-off-by: Binlong Gao --- .../search/CachingCollectorManager.java | 35 +++++----- .../search/TestCachingCollectorManager.java | 68 +++++++++++++++++++ .../grouping/AllGroupHeadsCollector.java | 1 + .../search/grouping/AllGroupsCollector.java | 4 +- .../search/grouping/GroupingSearch.java | 12 ---- .../grouping/BaseGroupSelectorTestCase.java | 16 ++--- 6 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java diff --git a/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java b/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java index f51b51ec6a10..22c8e77bd3de 100644 --- a/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java +++ b/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; /** * A {@link CollectorManager} that wraps a delegate {@link CollectorManager} and caches all @@ -35,7 +34,7 @@ * *
  * CachingCollectorManager<C1, R1> caching = new CachingCollectorManager<>(
- *     firstPassManager, cacheScores, maxRAMMB);
+ *     firstPassManager, cacheScores, maxRAMMB, null);
  * R1 firstResult = searcher.search(query, caching);
  *
  * if (caching.isCached()) {
@@ -48,17 +47,16 @@
  *
  * @lucene.experimental
  */
-public class CachingCollectorManager implements CollectorManager {
+public class CachingCollectorManager
+    implements CollectorManager {
 
   private final CollectorManager delegate;
   private final boolean cacheScores;
   private final Double maxRAMMB;
   private final Integer maxDocsToCache;
 
-  // One CachingCollector per slice, thread-safe for concurrent newCollector() calls.
-  private final List cachingCollectors = new CopyOnWriteArrayList<>();
-  // The original unwrapped collectors
-  private final List originalCollectors = new CopyOnWriteArrayList<>();
+  // One CachingCollector per slice
+  private final List cachingCollectors = new ArrayList<>();
 
   /**
    * @param delegate the first-pass {@link CollectorManager}
@@ -82,27 +80,30 @@ public CachingCollectorManager(
   }
 
   @Override
-  public C newCollector() throws IOException {
+  public CachingCollector newCollector() throws IOException {
     C collector = delegate.newCollector();
-    originalCollectors.add(collector);
     CachingCollector cache =
         maxDocsToCache != null
             ? CachingCollector.create(collector, cacheScores, maxDocsToCache)
             : CachingCollector.create(collector, cacheScores, maxRAMMB);
     cachingCollectors.add(cache);
-    @SuppressWarnings("unchecked")
-    C wrapped = (C) cache;
-    return wrapped;
+    return cache;
   }
 
   @Override
-  public R reduce(Collection collectors) throws IOException {
-    return delegate.reduce(originalCollectors);
+  @SuppressWarnings("unchecked")
+  public R reduce(Collection collectors) throws IOException {
+    List originals = new ArrayList<>(collectors.size());
+    for (CachingCollector cache : collectors) {
+      originals.add((C) cache.in);
+    }
+    return delegate.reduce(originals);
   }
 
   /**
-   * Returns {@code true} if all per-slice caches are intact (none overflowed their RAM budget),
-   * meaning {@link #replay} can be called.
+   * Returns {@code true} if the search has been run and all per-slice caches are intact (none
+   * overflowed their RAM/doc budget). Returns {@code false} if the search has not yet been run or
+   * any cache overflowed.
    */
   public boolean isCached() {
     return !cachingCollectors.isEmpty()
@@ -112,7 +113,7 @@ public boolean isCached() {
   /**
    * Replays each per-slice cache into a fresh second-pass collector, then reduces all results.
    *
-   * @throws IllegalStateException if any slice cache is not available
+   * @throws IllegalStateException if {@link #isCached()} returns {@code false}
    */
   public  R2 replay(CollectorManager secondPassManager)
       throws IOException {
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java b/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
new file mode 100644
index 000000000000..2f510a1cd523
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.search;
+
+import java.io.IOException;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.tests.index.RandomIndexWriter;
+import org.apache.lucene.tests.util.LuceneTestCase;
+
+public class TestCachingCollectorManager extends LuceneTestCase {
+
+  public void testCacheOverflow() throws IOException {
+    Directory dir = newDirectory();
+    RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
+    for (int i = 0; i < atLeast(10); i++) {
+      iw.addDocument(new Document());
+    }
+    IndexSearcher searcher = newSearcher(iw.getReader());
+    iw.close();
+
+    CachingCollectorManager caching =
+        new CachingCollectorManager<>(
+            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, 0);
+
+    searcher.search(MatchAllDocsQuery.INSTANCE, caching);
+    assertFalse(caching.isCached());
+    assertThrows(
+        IllegalStateException.class,
+        () -> caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE)));
+
+    searcher.getIndexReader().close();
+    dir.close();
+  }
+
+  public void testNotCachedBeforeSearch() {
+    CachingCollectorManager caching =
+        new CachingCollectorManager<>(
+            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, Integer.MAX_VALUE);
+    assertFalse(caching.isCached());
+
+    assertThrows(
+        IllegalStateException.class,
+        () -> caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE)));
+  }
+
+  public void testConstructorValidation() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            new CachingCollectorManager<>(
+                new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, null));
+  }
+}
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 013f83b3f3c6..04c80c727a3a 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
@@ -173,6 +173,7 @@ protected void doSetNextReader(LeafReaderContext context) throws IOException {
   @Override
   public void setScorer(Scorable scorer) throws IOException {
     this.scorer = scorer;
+    groupSelector.setScorer(scorer);
     for (GroupHead head : heads.values()) {
       head.setScorer(scorer);
     }
diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupsCollector.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupsCollector.java
index d1b700cef1cb..4aa5301a0af6 100644
--- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupsCollector.java
+++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupsCollector.java
@@ -70,7 +70,9 @@ public Collection getGroups() {
   }
 
   @Override
-  public void setScorer(Scorable scorer) throws IOException {}
+  public void setScorer(Scorable scorer) throws IOException {
+    groupSelector.setScorer(scorer);
+  }
 
   @Override
   protected void doSetNextReader(LeafReaderContext context) throws IOException {
diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
index 5dfc94019efc..4f5aaf2a12e2 100644
--- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
+++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
@@ -86,18 +86,6 @@ public GroupingSearch(Supplier> grouperFactory) {
     this(grouperFactory, null);
   }
 
-  /**
-   * Constructs a GroupingSearch instance that groups documents using a {@link
-   * GroupSelector}
-   *
-   * @param groupSelector a {@link GroupSelector} that defines groups for this GroupingSearch
-   * @deprecated Use {@link #GroupingSearch(Supplier)} to provide a factory for concurrent search
-   */
-  @Deprecated
-  public GroupingSearch(GroupSelector groupSelector) {
-    this(() -> groupSelector, null);
-  }
-
   /**
    * Constructs a GroupingSearch instance that groups documents by function using a
    * {@link ValueSource} instance.
diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java
index f82da65c0c16..936866668eec 100644
--- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java
@@ -57,7 +57,7 @@ public void testSortByRelevance() throws IOException {
     Query topLevel = new TermQuery(new Term("text", query[random().nextInt(query.length)]));
 
     IndexSearcher searcher = shard.getIndexSearcher();
-    GroupingSearch grouper = new GroupingSearch(getGroupSelector());
+    GroupingSearch grouper = new GroupingSearch(this::getGroupSelector);
     grouper.setGroupDocsLimit(10);
     TopGroups topGroups = grouper.search(searcher, topLevel, 0, 5);
     TopDocs topDoc = searcher.search(topLevel, 1);
@@ -89,7 +89,7 @@ public void testSortGroups() throws IOException {
     String[] query = new String[] {"foo", "bar", "baz"};
     Query topLevel = new TermQuery(new Term("text", query[random().nextInt(query.length)]));
 
-    GroupingSearch grouper = new GroupingSearch(getGroupSelector());
+    GroupingSearch grouper = new GroupingSearch(this::getGroupSelector);
     grouper.setGroupDocsLimit(10);
     Sort sort =
         new Sort(
@@ -132,7 +132,7 @@ public void testSortWithinGroups() throws IOException {
     String[] query = new String[] {"foo", "bar", "baz"};
     Query topLevel = new TermQuery(new Term("text", query[random().nextInt(query.length)]));
 
-    GroupingSearch grouper = new GroupingSearch(getGroupSelector());
+    GroupingSearch grouper = new GroupingSearch(this::getGroupSelector);
     grouper.setGroupDocsLimit(10);
     Sort sort =
         new Sort(
@@ -175,8 +175,7 @@ public void testGroupHeads() throws IOException {
     String[] query = new String[] {"foo", "bar", "baz"};
     Query topLevel = new TermQuery(new Term("text", query[random().nextInt(query.length)]));
 
-    GroupSelector groupSelector = getGroupSelector();
-    GroupingSearch grouping = new GroupingSearch(groupSelector);
+    GroupingSearch grouping = new GroupingSearch(this::getGroupSelector);
     grouping.setAllGroups(true);
     grouping.setAllGroupHeads(true);
 
@@ -235,8 +234,7 @@ public void testGroupHeadsWithSort() throws IOException {
         new Sort(
             new SortField("sort1", SortField.Type.STRING),
             new SortField("sort2", SortField.Type.LONG));
-    GroupSelector groupSelector = getGroupSelector();
-    GroupingSearch grouping = new GroupingSearch(groupSelector);
+    GroupingSearch grouping = new GroupingSearch(this::getGroupSelector);
     grouping.setAllGroups(true);
     grouping.setAllGroupHeads(true);
     grouping.setSortWithinGroup(sort);
@@ -417,12 +415,12 @@ public void testIgnoreDocsWithoutGroupField() throws IOException {
     Query query = new TermQuery(new Term("text", "foo"));
 
     // Test default behavior (include null group)
-    GroupingSearch grouping1 = new GroupingSearch(getGroupSelector());
+    GroupingSearch grouping1 = new GroupingSearch(this::getGroupSelector);
     TopGroups groups1 = grouping1.search(searcher, query, 0, 10);
     int defaultGroupCount = groups1.groups.length;
 
     // Test ignoring docs without group field
-    GroupingSearch grouping2 = new GroupingSearch(getGroupSelector());
+    GroupingSearch grouping2 = new GroupingSearch(this::getGroupSelector);
     grouping2.setIgnoreDocsWithoutGroupField(true);
     TopGroups groups2 = grouping2.search(searcher, query, 0, 10);
     int ignoreGroupCount = groups2.groups.length;

From 22efafaf81da33b78d2b4ee5eefcd00695c2aeee Mon Sep 17 00:00:00 2001
From: Binlong Gao 
Date: Wed, 17 Jun 2026 16:20:05 +0800
Subject: [PATCH 4/7] Fix test issue

---
 .../search/grouping/BaseGroupSelectorTestCase.java       | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java
index 936866668eec..aed00a9d4712 100644
--- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java
@@ -32,6 +32,7 @@
 import org.apache.lucene.search.FieldDoc;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.TermQuery;
@@ -72,7 +73,6 @@ public void testSortByRelevance() throws IOException {
       TopDocs td = searcher.search(filtered, 10);
       assertScoreDocsEquals(topGroups.groups[i].scoreDocs(), td.scoreDocs);
       if (i == 0) {
-        assertEquals(td.scoreDocs[0].doc, topDoc.scoreDocs[0].doc);
         assertEquals(td.scoreDocs[0].score, topDoc.scoreDocs[0].score, 0);
       }
     }
@@ -435,6 +435,13 @@ public void testIgnoreDocsWithoutGroupField() throws IOException {
     shard.close();
   }
 
+  protected static void assertScoreDocsEquals(ScoreDoc[] expected, ScoreDoc[] actual) {
+    assertEquals(expected.length, actual.length);
+    for (int i = 0; i < expected.length; i++) {
+      assertEquals(expected[i].score, actual[i].score, 0);
+    }
+  }
+
   private void assertSortsBefore(GroupDocs first, GroupDocs second) {
     Object[] groupSortValues = second.groupSortValues();
     Object[] prevSortValues = first.groupSortValues();

From dab05cc76a808fa406ee657d98321bc59d92e9a3 Mon Sep 17 00:00:00 2001
From: Binlong Gao 
Date: Thu, 25 Jun 2026 11:41:56 +0800
Subject: [PATCH 5/7] Add more tests and fix some issue

Signed-off-by: Binlong Gao 
---
 .../lucene/search/CachingCollector.java       |  3 ++-
 .../search/TestCachingCollectorManager.java   | 18 ++++++++++++-
 .../search/grouping/TestGroupingSearch.java   | 25 +++++++------------
 3 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/lucene/core/src/java/org/apache/lucene/search/CachingCollector.java b/lucene/core/src/java/org/apache/lucene/search/CachingCollector.java
index 07aa3581e71c..57311aa601da 100644
--- a/lucene/core/src/java/org/apache/lucene/search/CachingCollector.java
+++ b/lucene/core/src/java/org/apache/lucene/search/CachingCollector.java
@@ -329,7 +329,8 @@ public static CachingCollector create(Collector other, boolean cacheScores, int
         : new NoScoreCachingCollector(other, maxDocsToCache);
   }
 
-  private boolean cached;
+  // visible for other threads in concurrent search mode
+  private volatile boolean cached;
 
   private CachingCollector(Collector in) {
     super(in);
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java b/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
index 2f510a1cd523..5789fa0ddce7 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
@@ -58,11 +58,27 @@ public void testNotCachedBeforeSearch() {
         () -> caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE)));
   }
 
-  public void testConstructorValidation() {
+  public void testConstructor() {
     assertThrows(
         IllegalArgumentException.class,
         () ->
             new CachingCollectorManager<>(
                 new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, null));
+
+    CachingCollectorManager caching =
+        new CachingCollectorManager<>(
+            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE),
+            random().nextBoolean(),
+            null,
+            Integer.MAX_VALUE);
+    assertFalse(caching.isCached());
+
+    caching =
+        new CachingCollectorManager<>(
+            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE),
+            random().nextBoolean(),
+            1.0,
+            null);
+    assertFalse(caching.isCached());
   }
 }
diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
index 7d39646a14cf..c35ad6736760 100644
--- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
@@ -58,21 +58,21 @@ public void testBasic() throws Exception {
     Document doc = new Document();
     addGroupField(doc, groupField, "author1", canUseIDV);
     doc.add(new TextField("content", "random text", Field.Store.YES));
-    doc.add(new Field("id", "1", customType));
+    doc.add(new Field("id", "0", customType));
     documents.add(doc);
 
     // 1
     doc = new Document();
     addGroupField(doc, groupField, "author1", canUseIDV);
     doc.add(new TextField("content", "some more random text", Field.Store.YES));
-    doc.add(new Field("id", "2", customType));
+    doc.add(new Field("id", "1", customType));
     documents.add(doc);
 
     // 2
     doc = new Document();
     addGroupField(doc, groupField, "author1", canUseIDV);
     doc.add(new TextField("content", "some more random textual data", Field.Store.YES));
-    doc.add(new Field("id", "3", customType));
+    doc.add(new Field("id", "2", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
     documents.add(doc);
     w.addDocuments(documents);
@@ -82,7 +82,7 @@ public void testBasic() throws Exception {
     doc = new Document();
     addGroupField(doc, groupField, "author2", canUseIDV);
     doc.add(new TextField("content", "some random text", Field.Store.YES));
-    doc.add(new Field("id", "4", customType));
+    doc.add(new Field("id", "3", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
     w.addDocument(doc);
 
@@ -90,14 +90,14 @@ public void testBasic() throws Exception {
     doc = new Document();
     addGroupField(doc, groupField, "author3", canUseIDV);
     doc.add(new TextField("content", "some more random text", Field.Store.YES));
-    doc.add(new Field("id", "5", customType));
+    doc.add(new Field("id", "4", customType));
     documents.add(doc);
 
     // 5
     doc = new Document();
     addGroupField(doc, groupField, "author3", canUseIDV);
     doc.add(new TextField("content", "random", Field.Store.YES));
-    doc.add(new Field("id", "6", customType));
+    doc.add(new Field("id", "5", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
     documents.add(doc);
     w.addDocuments(documents);
@@ -106,17 +106,10 @@ public void testBasic() throws Exception {
     // 6 -- no author field
     doc = new Document();
     doc.add(new TextField("content", "random word stuck in alot of other text", Field.Store.YES));
-    doc.add(new Field("id", "7", customType));
-    documents.add(doc);
-
-    // 7 -- no match document
-    doc = new Document();
-    doc.add(new TextField("content", "no-match", Field.Store.YES));
-    doc.add(new Field("id", "8", customType));
+    doc.add(new Field("id", "6", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
-    documents.add(doc);
-    w.addDocuments(documents);
-    documents.clear();
+
+    w.addDocument(doc);
 
     IndexSearcher indexSearcher = shard.getIndexSearcher();
     indexSearcher.setSimilarity(new BM25Similarity());

From 5d85a911ca46776d70cde09bd8b5ee5eb7b1b7e2 Mon Sep 17 00:00:00 2001
From: Binlong Gao 
Date: Fri, 26 Jun 2026 22:43:00 +0800
Subject: [PATCH 6/7] Add more tests and fix some issues

Signed-off-by: Binlong Gao 
---
 lucene/CHANGES.txt                            |  5 +--
 .../search/CachingCollectorManager.java       |  4 +-
 .../search/TestCachingCollectorManager.java   | 45 ++++++++++++-------
 .../search/grouping/TestGroupingSearch.java   | 14 +++---
 4 files changed, 41 insertions(+), 27 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index b87793134d41..39a821ac1606 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -287,7 +287,8 @@ New Features
 
 Improvements
 ---------------------
-(No changes)
+
+* GITHUB#16247: Introduce CachingCollectorManager to parallelize search when using CachingCollector and remove useless GroupingSearch constructor (Binlong Gao)
 
 Optimizations
 ---------------------
@@ -424,8 +425,6 @@ Improvements
 
 * GITHUB#15660: Introduce LargeNumHitsTopDocsCollectorManager to parallelize search when using LargeNumHitsTopDocsCollector. (Binlong Gao)
 
-* GITHUB#16247: Introduce CachingCollectorManager to parallelize search when using CachingCollector. (Binlong Gao)
-
 * GITHUB#16088: SearchGroup#merge now returns an empty collection instead of null when topGroups is empty. (Luca Cavanna)
 
 * GITHUB#16144: Clarify LeafCollector batch collection scoring contract. (Costin Leau)
diff --git a/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java b/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java
index 22c8e77bd3de..09a3be7b363c 100644
--- a/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java
+++ b/lucene/core/src/java/org/apache/lucene/search/CachingCollectorManager.java
@@ -70,8 +70,8 @@ public CachingCollectorManager(
       boolean cacheScores,
       Double maxRAMMB,
       Integer maxDocsToCache) {
-    if (maxRAMMB == null && maxDocsToCache == null) {
-      throw new IllegalArgumentException("Either maxRAMMB or maxDocsToCache must be set");
+    if (maxRAMMB == null && maxDocsToCache == null || maxRAMMB != null && maxDocsToCache != null) {
+      throw new IllegalArgumentException("Exactly one of maxRAMMB or maxDocsToCache must be set");
     }
     this.delegate = delegate;
     this.cacheScores = cacheScores;
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java b/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
index 5789fa0ddce7..530d64bab1f4 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestCachingCollectorManager.java
@@ -58,6 +58,31 @@ public void testNotCachedBeforeSearch() {
         () -> caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE)));
   }
 
+  public void testBasic() throws IOException {
+    Directory dir = newDirectory();
+    RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
+    for (int i = 0; i < 10; i++) {
+      iw.addDocument(new Document());
+    }
+    IndexSearcher searcher = newSearcher(iw.getReader());
+    iw.close();
+
+    CachingCollectorManager caching =
+        new CachingCollectorManager<>(
+            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), true, null, Integer.MAX_VALUE);
+
+    TopDocs firstResult = searcher.search(MatchAllDocsQuery.INSTANCE, caching);
+    assertTrue(caching.isCached());
+    assertEquals(10, firstResult.totalHits.value());
+
+    TopDocs replayResult = caching.replay(new TopScoreDocCollectorManager(10, Integer.MAX_VALUE));
+    assertEquals(firstResult.totalHits.value(), replayResult.totalHits.value());
+    assertEquals(firstResult.scoreDocs.length, replayResult.scoreDocs.length);
+
+    searcher.getIndexReader().close();
+    dir.close();
+  }
+
   public void testConstructor() {
     assertThrows(
         IllegalArgumentException.class,
@@ -65,20 +90,10 @@ public void testConstructor() {
             new CachingCollectorManager<>(
                 new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, null, null));
 
-    CachingCollectorManager caching =
-        new CachingCollectorManager<>(
-            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE),
-            random().nextBoolean(),
-            null,
-            Integer.MAX_VALUE);
-    assertFalse(caching.isCached());
-
-    caching =
-        new CachingCollectorManager<>(
-            new TopScoreDocCollectorManager(10, Integer.MAX_VALUE),
-            random().nextBoolean(),
-            1.0,
-            null);
-    assertFalse(caching.isCached());
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            new CachingCollectorManager<>(
+                new TopScoreDocCollectorManager(10, Integer.MAX_VALUE), false, 1.0, 1));
   }
 }
diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
index c35ad6736760..8029a79646fc 100644
--- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
@@ -58,21 +58,21 @@ public void testBasic() throws Exception {
     Document doc = new Document();
     addGroupField(doc, groupField, "author1", canUseIDV);
     doc.add(new TextField("content", "random text", Field.Store.YES));
-    doc.add(new Field("id", "0", customType));
+    doc.add(new Field("id", "1", customType));
     documents.add(doc);
 
     // 1
     doc = new Document();
     addGroupField(doc, groupField, "author1", canUseIDV);
     doc.add(new TextField("content", "some more random text", Field.Store.YES));
-    doc.add(new Field("id", "1", customType));
+    doc.add(new Field("id", "2", customType));
     documents.add(doc);
 
     // 2
     doc = new Document();
     addGroupField(doc, groupField, "author1", canUseIDV);
     doc.add(new TextField("content", "some more random textual data", Field.Store.YES));
-    doc.add(new Field("id", "2", customType));
+    doc.add(new Field("id", "3", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
     documents.add(doc);
     w.addDocuments(documents);
@@ -82,7 +82,7 @@ public void testBasic() throws Exception {
     doc = new Document();
     addGroupField(doc, groupField, "author2", canUseIDV);
     doc.add(new TextField("content", "some random text", Field.Store.YES));
-    doc.add(new Field("id", "3", customType));
+    doc.add(new Field("id", "4", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
     w.addDocument(doc);
 
@@ -90,14 +90,14 @@ public void testBasic() throws Exception {
     doc = new Document();
     addGroupField(doc, groupField, "author3", canUseIDV);
     doc.add(new TextField("content", "some more random text", Field.Store.YES));
-    doc.add(new Field("id", "4", customType));
+    doc.add(new Field("id", "5", customType));
     documents.add(doc);
 
     // 5
     doc = new Document();
     addGroupField(doc, groupField, "author3", canUseIDV);
     doc.add(new TextField("content", "random", Field.Store.YES));
-    doc.add(new Field("id", "5", customType));
+    doc.add(new Field("id", "6", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
     documents.add(doc);
     w.addDocuments(documents);
@@ -106,7 +106,7 @@ public void testBasic() throws Exception {
     // 6 -- no author field
     doc = new Document();
     doc.add(new TextField("content", "random word stuck in alot of other text", Field.Store.YES));
-    doc.add(new Field("id", "6", customType));
+    doc.add(new Field("id", "7", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
 
     w.addDocument(doc);

From 447287517118a645283236029eebefc19faeb65e Mon Sep 17 00:00:00 2001
From: Binlong Gao 
Date: Fri, 26 Jun 2026 22:47:18 +0800
Subject: [PATCH 7/7] Revert unnecessary change

Signed-off-by: Binlong Gao 
---
 .../org/apache/lucene/search/grouping/TestGroupingSearch.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
index 8029a79646fc..07de8a18d7d2 100644
--- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
+++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGroupingSearch.java
@@ -106,7 +106,7 @@ public void testBasic() throws Exception {
     // 6 -- no author field
     doc = new Document();
     doc.add(new TextField("content", "random word stuck in alot of other text", Field.Store.YES));
-    doc.add(new Field("id", "7", customType));
+    doc.add(new Field("id", "6", customType));
     doc.add(new StringField("groupend", "x", Field.Store.NO));
 
     w.addDocument(doc);