From d40d5d5a3e141d7ef740c88d9c079b26791b393b Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Tue, 13 Jan 2026 21:13:29 +0800 Subject: [PATCH 01/10] Introduce FirstPassGroupingCollectorManager Signed-off-by: Binlong Gao Format code Signed-off-by: Binlong Gao --- lucene/CHANGES.txt | 2 + .../FirstPassGroupingCollectorManager.java | 99 +++++++++++++++++++ .../grouping/BaseGroupSelectorTestCase.java | 16 +-- .../lucene/search/grouping/TestGrouping.java | 48 +++++---- 4 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 0d756b2087c0..69be5a1d13e3 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -110,6 +110,8 @@ Improvements * GITHUB#15225: Improve package documentation for org.apache.lucene.util. (Syed Mohammad Saad) +* GITHUB#15574: Introduce FirstPassGroupingCollectorManager to parallelize search when using FirstPassGroupingCollector. (Binlong Gao) + Optimizations --------------------- * GITHUB#15681, GITHUB#15833: Replace pre-sized array or empty array with lambda expression to call Collection#toArray. (Zhou Hui) diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java new file mode 100644 index 000000000000..7e24e6735c39 --- /dev/null +++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java @@ -0,0 +1,99 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; +import org.apache.lucene.search.CollectorManager; +import org.apache.lucene.search.Sort; + +/** A CollectorManager implementation for FirstPassGroupingCollector. */ +public class FirstPassGroupingCollectorManager + implements CollectorManager, Collection>> { + + private final Supplier> groupSelectorFactory; + private final Sort groupSort; + private final int topNGroups; + private final boolean ignoreDocsWithoutGroupField; + private final List> collectors; + + /** + * Creates a new FirstPassGroupingCollectorManager. + * + * @param groupSelectorFactory factory to create group selectors for each collector + * @param groupSort the sort to use for groups + * @param topNGroups the number of top groups to collect + */ + public FirstPassGroupingCollectorManager( + Supplier> groupSelectorFactory, Sort groupSort, int topNGroups) { + this(groupSelectorFactory, groupSort, topNGroups, false); + } + + /** + * Creates a new FirstPassGroupingCollectorManager. + * + * @param groupSelectorFactory factory to create group selectors for each collector + * @param groupSort the sort to use for groups + * @param topNGroups the number of top groups to collect + * @param ignoreDocsWithoutGroupField whether to ignore documents without a group field + */ + public FirstPassGroupingCollectorManager( + Supplier> groupSelectorFactory, + Sort groupSort, + int topNGroups, + boolean ignoreDocsWithoutGroupField) { + this.groupSelectorFactory = groupSelectorFactory; + this.groupSort = groupSort; + this.topNGroups = topNGroups; + this.ignoreDocsWithoutGroupField = ignoreDocsWithoutGroupField; + this.collectors = new ArrayList<>(); + } + + @Override + public FirstPassGroupingCollector newCollector() throws IOException { + FirstPassGroupingCollector collector = + new FirstPassGroupingCollector<>( + groupSelectorFactory.get(), groupSort, topNGroups, ignoreDocsWithoutGroupField); + collectors.add(collector); + return collector; + } + + @Override + public Collection> reduce(Collection> collectors) + throws IOException { + if (collectors.isEmpty()) { + return null; + } + + List>> allGroups = new ArrayList<>(); + for (FirstPassGroupingCollector collector : collectors) { + Collection> groups = collector.getTopGroups(0); + if (groups != null) { + allGroups.add(groups); + } + } + + return SearchGroup.merge(allGroups, 0, topNGroups, groupSort); + } + + public List> getCollectors() { + return collectors; + } +} 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 687c7080dba2..280edbea35fa 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 @@ -309,17 +309,17 @@ public void testShardedGrouping() throws IOException { // A grouped query run in two phases against the control should give us the same // result as the query run against shards and merged back together after each phase. - FirstPassGroupingCollector singletonFirstPass = - new FirstPassGroupingCollector<>(getGroupSelector(), sort, 5); - control.getIndexSearcher().search(topLevel, singletonFirstPass); - Collection> singletonGroups = singletonFirstPass.getTopGroups(0); + FirstPassGroupingCollectorManager firstPassGroupingCollectorManager = + new FirstPassGroupingCollectorManager<>(this::getGroupSelector, sort, 5); + Collection> singletonGroups = + control.getIndexSearcher().search(topLevel, firstPassGroupingCollectorManager); List>> shardGroups = new ArrayList<>(); for (Shard shard : shards) { - FirstPassGroupingCollector fc = - new FirstPassGroupingCollector<>(getGroupSelector(), sort, 5); - shard.getIndexSearcher().search(topLevel, fc); - shardGroups.add(fc.getTopGroups(0)); + FirstPassGroupingCollectorManager fcm = + new FirstPassGroupingCollectorManager<>(this::getGroupSelector, sort, 5); + Collection> topGroups = shard.getIndexSearcher().search(topLevel, fcm); + shardGroups.add(topGroups); } Collection> mergedGroups = SearchGroup.merge(shardGroups, 0, 5, sort); assertEquals(singletonGroups, mergedGroups); diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index 37ee890fbf1d..664bc9ede848 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -220,19 +220,20 @@ public void testIgnoreDocsWithoutGroupField() throws IOException { IndexSearcher searcher = newSearcher(reader); // Test default behavior (include null group) - FirstPassGroupingCollector collector1 = - new FirstPassGroupingCollector<>(new TermGroupSelector(groupField), Sort.RELEVANCE, 10); - searcher.search(MatchAllDocsQuery.INSTANCE, collector1); - Collection> groups1 = collector1.getTopGroups(0); + FirstPassGroupingCollectorManager firstPassGroupingCollectorManager1 = + new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector(groupField), Sort.RELEVANCE, 10); + Collection> groups1 = + searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager1); assertEquals(3, groups1.size()); // Should include null group // Test ignoring docs without group field - FirstPassGroupingCollector collector2 = - new FirstPassGroupingCollector<>( - new TermGroupSelector(groupField), Sort.RELEVANCE, 10, true); - searcher.search(MatchAllDocsQuery.INSTANCE, collector2); - Collection> groups2 = collector2.getTopGroups(0); + FirstPassGroupingCollectorManager firstPassGroupingCollectorManager2 = + new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector(groupField), Sort.RELEVANCE, 10, true); + Collection> groups2 = + searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager2); assertEquals(2, groups2.size()); // Should exclude null group @@ -258,10 +259,11 @@ public void testAllDocsWithoutGroupField() throws IOException { IndexSearcher searcher = newSearcher(reader); // Test ignoring docs without group field when all docs lack the field - FirstPassGroupingCollector collector = - new FirstPassGroupingCollector<>(new TermGroupSelector("group"), Sort.RELEVANCE, 10, true); - searcher.search(MatchAllDocsQuery.INSTANCE, collector); - Collection> groups = collector.getTopGroups(0); + FirstPassGroupingCollectorManager firstPassGroupingCollectorManager2 = + new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector("group"), Sort.RELEVANCE, 10, true); + Collection> groups = + searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager2); assertNull(groups); // Should return null when no groups found @@ -277,11 +279,13 @@ private FirstPassGroupingCollector createRandomFirstPassCollector( String groupField, Sort groupSort, int topDocs) throws IOException { if (random().nextBoolean()) { ValueSource vs = new BytesRefFieldSource(groupField); - return new FirstPassGroupingCollector<>( - new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, topDocs); + return new FirstPassGroupingCollectorManager<>( + () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, topDocs) + .newCollector(); } else { - return new FirstPassGroupingCollector<>( - new TermGroupSelector(groupField), groupSort, topDocs); + return new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector(groupField), groupSort, topDocs) + .newCollector(); } } @@ -294,11 +298,13 @@ private FirstPassGroupingCollector createFirstPassCollector( GroupSelector selector = firstPassGroupingCollector.getGroupSelector(); if (TermGroupSelector.class.isAssignableFrom(selector.getClass())) { ValueSource vs = new BytesRefFieldSource(groupField); - return new FirstPassGroupingCollector<>( - new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, topDocs); + return new FirstPassGroupingCollectorManager<>( + () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, topDocs) + .newCollector(); } else { - return new FirstPassGroupingCollector<>( - new TermGroupSelector(groupField), groupSort, topDocs); + return new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector(groupField), groupSort, topDocs) + .newCollector(); } } From 79b499cafb130f964aea26558f31c75a428c796c Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Tue, 7 Apr 2026 17:22:36 +0800 Subject: [PATCH 02/10] Return directly if only one collector exists Signed-off-by: Binlong Gao --- .../search/grouping/FirstPassGroupingCollectorManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java index 7e24e6735c39..ce366bc77b82 100644 --- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java +++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java @@ -82,6 +82,10 @@ public Collection> reduce(Collection>> allGroups = new ArrayList<>(); for (FirstPassGroupingCollector collector : collectors) { Collection> groups = collector.getTopGroups(0); From eb3720ad667c7d82dd4e3a6e59be9fccdd7ad608 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Tue, 19 May 2026 13:26:42 +0800 Subject: [PATCH 03/10] Replace collector with collector manager in TestGrouping Signed-off-by: Binlong Gao --- .../FirstPassGroupingCollectorManager.java | 66 +++--- .../grouping/BaseGroupSelectorTestCase.java | 4 +- .../lucene/search/grouping/TestGrouping.java | 193 ++++++++++++------ 3 files changed, 174 insertions(+), 89 deletions(-) diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java index ce366bc77b82..749dbbd9ff27 100644 --- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java +++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java @@ -24,26 +24,54 @@ import org.apache.lucene.search.CollectorManager; import org.apache.lucene.search.Sort; -/** A CollectorManager implementation for FirstPassGroupingCollector. */ +/** + * A CollectorManager implementation for {@link FirstPassGroupingCollector} that supports parallel + * collection and merges results across segments. + * + *

Example usage: + * + *

+ * IndexSearcher searcher = new IndexSearcher(reader);
+ * Sort groupSort = Sort.RELEVANCE;
+ * int topNGroups = 10;
+ *
+ * FirstPassGroupingCollectorManager<BytesRef> manager =
+ *     new FirstPassGroupingCollectorManager<>(
+ *         () -> new TermGroupSelector("category"),
+ *         groupSort,
+ *         0,
+ *         topNGroups);
+ *
+ * Collection<SearchGroup<BytesRef>> topGroups = searcher.search(query, manager);
+ *
+ * // topGroups can then be passed to SecondPassGroupingCollector for full group results
+ * 
+ * + * @lucene.experimental + */ public class FirstPassGroupingCollectorManager implements CollectorManager, Collection>> { private final Supplier> groupSelectorFactory; private final Sort groupSort; + private final int groupOffset; private final int topNGroups; private final boolean ignoreDocsWithoutGroupField; - private final List> collectors; /** * Creates a new FirstPassGroupingCollectorManager. * * @param groupSelectorFactory factory to create group selectors for each collector * @param groupSort the sort to use for groups + * @param groupOffset The offset in the collected groups * @param topNGroups the number of top groups to collect */ public FirstPassGroupingCollectorManager( - Supplier> groupSelectorFactory, Sort groupSort, int topNGroups) { - this(groupSelectorFactory, groupSort, topNGroups, false); + Supplier> groupSelectorFactory, + Sort groupSort, + int groupOffset, + int topNGroups) { + this(groupSelectorFactory, groupSort, groupOffset, topNGroups, false); } /** @@ -51,42 +79,36 @@ public FirstPassGroupingCollectorManager( * * @param groupSelectorFactory factory to create group selectors for each collector * @param groupSort the sort to use for groups + * @param groupOffset The offset in the collected groups * @param topNGroups the number of top groups to collect * @param ignoreDocsWithoutGroupField whether to ignore documents without a group field */ public FirstPassGroupingCollectorManager( Supplier> groupSelectorFactory, Sort groupSort, + int groupOffset, int topNGroups, boolean ignoreDocsWithoutGroupField) { this.groupSelectorFactory = groupSelectorFactory; this.groupSort = groupSort; + this.groupOffset = groupOffset; this.topNGroups = topNGroups; this.ignoreDocsWithoutGroupField = ignoreDocsWithoutGroupField; - this.collectors = new ArrayList<>(); } @Override public FirstPassGroupingCollector newCollector() throws IOException { - FirstPassGroupingCollector collector = - new FirstPassGroupingCollector<>( - groupSelectorFactory.get(), groupSort, topNGroups, ignoreDocsWithoutGroupField); - collectors.add(collector); - return collector; + return new FirstPassGroupingCollector<>( + groupSelectorFactory.get(), + groupSort, + groupOffset + topNGroups, + ignoreDocsWithoutGroupField); } @Override public Collection> reduce(Collection> collectors) throws IOException { - if (collectors.isEmpty()) { - return null; - } - - if (collectors.size() == 1) { - return collectors.iterator().next().getTopGroups(0); - } - - List>> allGroups = new ArrayList<>(); + final List>> allGroups = new ArrayList<>(); for (FirstPassGroupingCollector collector : collectors) { Collection> groups = collector.getTopGroups(0); if (groups != null) { @@ -94,10 +116,6 @@ public Collection> reduce(Collection> getCollectors() { - return collectors; + return SearchGroup.merge(allGroups, groupOffset, topNGroups, groupSort); } } 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 8fb6eb51c3c6..f82da65c0c16 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 @@ -310,14 +310,14 @@ public void testShardedGrouping() throws IOException { // result as the query run against shards and merged back together after each phase. FirstPassGroupingCollectorManager firstPassGroupingCollectorManager = - new FirstPassGroupingCollectorManager<>(this::getGroupSelector, sort, 5); + new FirstPassGroupingCollectorManager<>(this::getGroupSelector, sort, 0, 5); Collection> singletonGroups = control.getIndexSearcher().search(topLevel, firstPassGroupingCollectorManager); List>> shardGroups = new ArrayList<>(); for (Shard shard : shards) { FirstPassGroupingCollectorManager fcm = - new FirstPassGroupingCollectorManager<>(this::getGroupSelector, sort, 5); + new FirstPassGroupingCollectorManager<>(this::getGroupSelector, sort, 0, 5); Collection> topGroups = shard.getIndexSearcher().search(topLevel, fcm); shardGroups.add(topGroups); } diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index 7808c3a3951a..e86663f6ce46 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -46,6 +46,7 @@ import org.apache.lucene.queries.function.valuesource.BytesRefFieldSource; import org.apache.lucene.search.CachingCollector; import org.apache.lucene.search.Collector; +import org.apache.lucene.search.CollectorManager; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.FieldDoc; import org.apache.lucene.search.IndexSearcher; @@ -142,15 +143,28 @@ public void testBasic() throws Exception { final Sort groupSort = Sort.RELEVANCE; - final FirstPassGroupingCollector c1 = - createRandomFirstPassCollector(groupField, groupSort, 10); - indexSearcher.search(new TermQuery(new Term("content", "random")), c1); + // TermGroupSelector or ValueSourceGroupSelector + final boolean isTermGroupSelector = random().nextBoolean(); + final FirstPassGroupingCollectorManager firstPassGroupingCollectorManager = + createFirstPassCollectorManager(isTermGroupSelector, groupField, groupSort, 0, 10); + Collection topGroups = + indexSearcher.search( + new TermQuery(new Term("content", "random")), firstPassGroupingCollectorManager); + + final TopGroupsCollectorManager topGroupsCollectorManager = + createSecondPassCollectorManager( + isTermGroupSelector, + groupField, + toByteRefSearchGroups(topGroups), + groupSort, + Sort.RELEVANCE, + 0, + 5, + true); + final TopGroups groups = + indexSearcher.search( + new TermQuery(new Term("content", "random")), topGroupsCollectorManager); - final TopGroupsCollector c2 = - createSecondPassCollector(c1, groupSort, Sort.RELEVANCE, 0, 5, true); - indexSearcher.search(new TermQuery(new Term("content", "random")), c2); - - final TopGroups groups = c2.getTopGroups(0); assertFalse(Float.isNaN(groups.maxScore)); assertEquals(7, groups.totalHitCount); @@ -222,7 +236,7 @@ public void testIgnoreDocsWithoutGroupField() throws IOException { // Test default behavior (include null group) FirstPassGroupingCollectorManager firstPassGroupingCollectorManager1 = new FirstPassGroupingCollectorManager<>( - () -> new TermGroupSelector(groupField), Sort.RELEVANCE, 10); + () -> new TermGroupSelector(groupField), Sort.RELEVANCE, 0, 10); Collection> groups1 = searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager1); @@ -231,7 +245,7 @@ public void testIgnoreDocsWithoutGroupField() throws IOException { // Test ignoring docs without group field FirstPassGroupingCollectorManager firstPassGroupingCollectorManager2 = new FirstPassGroupingCollectorManager<>( - () -> new TermGroupSelector(groupField), Sort.RELEVANCE, 10, true); + () -> new TermGroupSelector(groupField), Sort.RELEVANCE, 0, 10, true); Collection> groups2 = searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager2); @@ -259,11 +273,11 @@ public void testAllDocsWithoutGroupField() throws IOException { IndexSearcher searcher = newSearcher(reader); // Test ignoring docs without group field when all docs lack the field - FirstPassGroupingCollectorManager firstPassGroupingCollectorManager2 = + FirstPassGroupingCollectorManager firstPassGroupingCollectorManager = new FirstPassGroupingCollectorManager<>( - () -> new TermGroupSelector("group"), Sort.RELEVANCE, 10, true); + () -> new TermGroupSelector("group"), Sort.RELEVANCE, 0, 10, true); Collection> groups = - searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager2); + searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager); assertNull(groups); // Should return null when no groups found @@ -279,32 +293,31 @@ private FirstPassGroupingCollector createRandomFirstPassCollector( String groupField, Sort groupSort, int topDocs) throws IOException { if (random().nextBoolean()) { ValueSource vs = new BytesRefFieldSource(groupField); + Map context = new HashMap<>(); return new FirstPassGroupingCollectorManager<>( - () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, topDocs) + () -> new ValueSourceGroupSelector(vs, context), groupSort, 0, topDocs) .newCollector(); } else { return new FirstPassGroupingCollectorManager<>( - () -> new TermGroupSelector(groupField), groupSort, topDocs) + () -> new TermGroupSelector(groupField), groupSort, 0, topDocs) .newCollector(); } } - private FirstPassGroupingCollector createFirstPassCollector( + private FirstPassGroupingCollectorManager createFirstPassCollectorManager( + boolean isTermGroupSelector, String groupField, Sort groupSort, - int topDocs, - FirstPassGroupingCollector firstPassGroupingCollector) - throws IOException { - GroupSelector selector = firstPassGroupingCollector.getGroupSelector(); - if (TermGroupSelector.class.isAssignableFrom(selector.getClass())) { - ValueSource vs = new BytesRefFieldSource(groupField); + int groupOffset, + int topNGroups) { + if (isTermGroupSelector) { return new FirstPassGroupingCollectorManager<>( - () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, topDocs) - .newCollector(); + () -> new TermGroupSelector(groupField), groupSort, groupOffset, topNGroups); } else { + ValueSource vs = new BytesRefFieldSource(groupField); + Map context = new HashMap<>(); return new FirstPassGroupingCollectorManager<>( - () -> new TermGroupSelector(groupField), groupSort, topDocs) - .newCollector(); + () -> new ValueSourceGroupSelector(vs, context), groupSort, groupOffset, topNGroups); } } @@ -329,24 +342,25 @@ private TopGroupsCollector createSecondPassCollector( } // Basically converts searchGroups from MutableValue to BytesRef if grouping by ValueSource - @SuppressWarnings("unchecked") - private TopGroupsCollector createSecondPassCollector( - FirstPassGroupingCollector firstPassGroupingCollector, + private TopGroupsCollectorManager createSecondPassCollectorManager( + boolean isTermGroupSelector, String groupField, Collection> searchGroups, Sort groupSort, Sort sortWithinGroup, + int withinGroupOffset, int maxDocsPerGroup, boolean getMaxScores) throws IOException { - if (firstPassGroupingCollector - .getGroupSelector() - .getClass() - .isAssignableFrom(TermGroupSelector.class)) { - GroupSelector selector = - (GroupSelector) firstPassGroupingCollector.getGroupSelector(); - return new TopGroupsCollector<>( - selector, searchGroups, groupSort, sortWithinGroup, maxDocsPerGroup, getMaxScores); + if (isTermGroupSelector) { + return new TopGroupsCollectorManager<>( + () -> new TermGroupSelector(groupField), + searchGroups, + groupSort, + sortWithinGroup, + withinGroupOffset, + maxDocsPerGroup, + getMaxScores); } else { ValueSource vs = new BytesRefFieldSource(groupField); List> mvalSearchGroups = new ArrayList<>(searchGroups.size()); @@ -362,9 +376,15 @@ private TopGroupsCollector createSecondPassCollector( sg.sortValues = mergedTopGroup.sortValues; mvalSearchGroups.add(sg); } - ValueSourceGroupSelector selector = new ValueSourceGroupSelector(vs, new HashMap<>()); - return new TopGroupsCollector<>( - selector, mvalSearchGroups, groupSort, sortWithinGroup, maxDocsPerGroup, getMaxScores); + Map context = new HashMap<>(); + return new TopGroupsCollectorManager<>( + () -> new ValueSourceGroupSelector(vs, context), + mvalSearchGroups, + groupSort, + sortWithinGroup, + withinGroupOffset, + maxDocsPerGroup, + getMaxScores); } } @@ -428,6 +448,55 @@ private Collection> getSearchGroups( return null; } + @SuppressWarnings("unchecked") + private Collection> toByteRefSearchGroups(Collection groups) { + if (groups == null + || groups.isEmpty() + || !(((SearchGroup) groups.iterator().next()).groupValue instanceof MutableValue)) { + return (Collection>) groups; + } + List> result = new ArrayList<>(groups.size()); + for (Object g : groups) { + SearchGroup sg = (SearchGroup) g; + SearchGroup out = new SearchGroup<>(); + MutableValue mv = (MutableValue) sg.groupValue; + out.groupValue = mv.exists() ? ((MutableValueStr) mv).value.get() : null; + out.sortValues = sg.sortValues; + result.add(out); + } + return result; + } + + @SuppressWarnings("unchecked") + private TopGroups toByteTopGroups(TopGroups topGroups) { + if (topGroups == null + || topGroups.groups.length == 0 + || !(topGroups.groups[0].groupValue() instanceof MutableValue)) { + return (TopGroups) topGroups; + } + List> groups = new ArrayList<>(topGroups.groups.length); + final GroupDocs[] mvalTopGroups = (GroupDocs[]) topGroups.groups; + for (GroupDocs mvalGd : mvalTopGroups) { + BytesRef groupValue = + mvalGd.groupValue().exists() ? ((MutableValueStr) mvalGd.groupValue()).value.get() : null; + groups.add( + new GroupDocs<>( + Float.NaN, + mvalGd.maxScore(), + mvalGd.totalHits(), + mvalGd.scoreDocs(), + groupValue, + mvalGd.groupSortValues())); + } + return new TopGroups<>( + topGroups.groupSort, + topGroups.withinGroupSort, + topGroups.totalHitCount, + topGroups.totalGroupedHitCount, + groups.toArray(GroupDocs[]::new), + Float.NaN); + } + @SuppressWarnings({"unchecked", "rawtypes"}) private TopGroups getTopGroups(TopGroupsCollector c, int withinGroupOffset) { if (c.getGroupSelector().getClass().isAssignableFrom(TermGroupSelector.class)) { @@ -1467,30 +1536,23 @@ private TopGroups searchShards( : ScoreMode.COMPLETE_NO_SCORES, 1); final List>> shardGroups = new ArrayList<>(); - List> firstPassGroupingCollectors = new ArrayList<>(); - FirstPassGroupingCollector firstPassCollector = null; - String groupField = "group"; + // TermGroupSelector or ValueSourceGroupSelector + final boolean isTermGroupSelector = random().nextBoolean(); + for (int shardIDX = 0; shardIDX < subSearchers.length; shardIDX++) { + final FirstPassGroupingCollectorManager firstPassGroupingCollectorManager = + createFirstPassCollectorManager( + isTermGroupSelector, groupField, groupSort, 0, groupOffset + topNGroups); - // First shard determines whether we use IDV or not; - // all other shards match that: - if (firstPassCollector == null) { - firstPassCollector = - createRandomFirstPassCollector(groupField, groupSort, groupOffset + topNGroups); - } else { - firstPassCollector = - createFirstPassCollector( - groupField, groupSort, groupOffset + topNGroups, firstPassCollector); - } if (VERBOSE) { System.out.println(" shard=" + shardIDX + " groupField=" + groupField); - System.out.println(" 1st pass collector=" + firstPassCollector); + System.out.println(" 1st pass collector manager=" + firstPassGroupingCollectorManager); } - firstPassGroupingCollectors.add(firstPassCollector); - subSearchers[shardIDX].search(w, firstPassCollector); - final Collection> topGroups = getSearchGroups(firstPassCollector, 0); + final Collection> topGroups = + toByteRefSearchGroups( + subSearchers[shardIDX].search(query, firstPassGroupingCollectorManager)); if (topGroups != null) { if (VERBOSE) { System.out.println( @@ -1536,17 +1598,19 @@ private TopGroups searchShards( // Now 2nd pass: final List> shardTopGroups = new ArrayList<>(subSearchers.length); for (int shardIDX = 0; shardIDX < subSearchers.length; shardIDX++) { - final TopGroupsCollector secondPassCollector = - createSecondPassCollector( - firstPassGroupingCollectors.get(shardIDX), + final TopGroupsCollectorManager secondPassCollectorManager = + createSecondPassCollectorManager( + isTermGroupSelector, groupField, mergedTopGroups, groupSort, docSort, + 0, docOffset + topNDocs, getMaxScores); - subSearchers[shardIDX].search(w, secondPassCollector); - shardTopGroups.add(getTopGroups(secondPassCollector, 0)); + final TopGroups topGroupsRaw = + subSearchers[shardIDX].search(w, secondPassCollectorManager); + shardTopGroups.add(toByteTopGroups(topGroupsRaw)); if (VERBOSE) { System.out.println( " " @@ -1670,8 +1734,11 @@ public ShardSearcher(LeafReaderContext ctx, IndexReaderContext parent) { this.ctx = ctx; } - public void search(Weight weight, Collector collector) throws IOException { + public T search(Weight weight, CollectorManager collectorManager) + throws IOException { + final C collector = collectorManager.newCollector(); searchLeaf(ctx, 0, DocIdSetIterator.NO_MORE_DOCS, weight, collector); + return collectorManager.reduce(Collections.singletonList(collector)); } @Override From 21d6c6dbdf312df2298bb61ee30bada6d66ca4c3 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Tue, 19 May 2026 15:35:51 +0800 Subject: [PATCH 04/10] Move changelog to 10.5.0 Signed-off-by: Binlong Gao --- lucene/CHANGES.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 49cb3808f6bd..79b4e62de439 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -135,8 +135,6 @@ Improvements * GITHUB#15225: Improve package documentation for org.apache.lucene.util. (Syed Mohammad Saad) -* GITHUB#15574: Introduce FirstPassGroupingCollectorManager to parallelize search when using FirstPassGroupingCollector. (Binlong Gao) - * GITHUB#15558: Refactor QueryCache for performance. (Sagar Upadhyaya) Optimizations @@ -337,6 +335,8 @@ Improvements * GITHUB#16000: Clarify that Accountable#ramBytesUsed() reports JVM heap memory only. (Luca Cavanna) +* GITHUB#15574: Introduce FirstPassGroupingCollectorManager to parallelize search when using FirstPassGroupingCollector. (Binlong Gao) + Optimizations --------------------- * GITHUB#15861: Optimise PhraseScorer by short circuiting non competitive documents in TOP_SCORES mode. (Prithvi S) From e09fc0077736f9824ab7ac15fbb79cb1af6dd7c8 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Wed, 20 May 2026 14:01:10 +0800 Subject: [PATCH 05/10] Do not share context across collectors in concurrent scenario Signed-off-by: Binlong Gao --- .../org/apache/lucene/search/grouping/TestGrouping.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index e86663f6ce46..26326d8ee369 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -293,9 +293,8 @@ private FirstPassGroupingCollector createRandomFirstPassCollector( String groupField, Sort groupSort, int topDocs) throws IOException { if (random().nextBoolean()) { ValueSource vs = new BytesRefFieldSource(groupField); - Map context = new HashMap<>(); return new FirstPassGroupingCollectorManager<>( - () -> new ValueSourceGroupSelector(vs, context), groupSort, 0, topDocs) + () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, 0, topDocs) .newCollector(); } else { return new FirstPassGroupingCollectorManager<>( @@ -315,9 +314,8 @@ private FirstPassGroupingCollectorManager createFirstPassCollectorManager( () -> new TermGroupSelector(groupField), groupSort, groupOffset, topNGroups); } else { ValueSource vs = new BytesRefFieldSource(groupField); - Map context = new HashMap<>(); return new FirstPassGroupingCollectorManager<>( - () -> new ValueSourceGroupSelector(vs, context), groupSort, groupOffset, topNGroups); + () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, groupOffset, topNGroups); } } @@ -376,9 +374,8 @@ private TopGroupsCollectorManager createSecondPassCollectorManager( sg.sortValues = mergedTopGroup.sortValues; mvalSearchGroups.add(sg); } - Map context = new HashMap<>(); return new TopGroupsCollectorManager<>( - () -> new ValueSourceGroupSelector(vs, context), + () -> new ValueSourceGroupSelector(vs, new HashMap<>()), mvalSearchGroups, groupSort, sortWithinGroup, From 3dac819be2f534a3ab1845878fdf3dcecbf7bf02 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Wed, 20 May 2026 14:08:11 +0800 Subject: [PATCH 06/10] Format code Signed-off-by: Binlong Gao --- .../test/org/apache/lucene/search/grouping/TestGrouping.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index 26326d8ee369..686f3ca90687 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -315,7 +315,10 @@ private FirstPassGroupingCollectorManager createFirstPassCollectorManager( } else { ValueSource vs = new BytesRefFieldSource(groupField); return new FirstPassGroupingCollectorManager<>( - () -> new ValueSourceGroupSelector(vs, new HashMap<>()), groupSort, groupOffset, topNGroups); + () -> new ValueSourceGroupSelector(vs, new HashMap<>()), + groupSort, + groupOffset, + topNGroups); } } From 725889fa0134e432c505a9edd66cd246d6b938dc Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Thu, 21 May 2026 21:25:12 +0800 Subject: [PATCH 07/10] Add more tests Signed-off-by: Binlong Gao --- lucene/CHANGES.txt | 2 +- .../FirstPassGroupingCollectorManager.java | 10 +++- .../lucene/search/grouping/TestGrouping.java | 46 ++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 0467412b8e07..d8f7379252b5 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -336,7 +336,7 @@ Improvements * GITHUB#15976: Store DocValuesSkipper data in their own file. (Alan Woodward) -* GITHUB#15574, GITHUB#15995: Introduce TopGroupsCollectorManager to parallelize search when using TopGroupsCollector. (Binlong Gao) +* GITHUB#15603, GITHUB#15995: Introduce TopGroupsCollectorManager to parallelize search when using TopGroupsCollector. (Binlong Gao) * GITHUB#15565: Introduce AllGroupHeadsCollectorManager to parallelize search when using AllGroupHeadsCollector. (Binlong Gao) diff --git a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java index 749dbbd9ff27..3c62ad121946 100644 --- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java +++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollectorManager.java @@ -42,9 +42,9 @@ * 0, * topNGroups); * - * Collection<SearchGroup<BytesRef>> topGroups = searcher.search(query, manager); + * Collection<SearchGroup<BytesRef>> searchGroups = searcher.search(query, manager); * - * // topGroups can then be passed to SecondPassGroupingCollector for full group results + * // searchGroups can then be passed to a second pass collector manager like TopGroupsCollectorManager for full group results * * * @lucene.experimental @@ -89,6 +89,12 @@ public FirstPassGroupingCollectorManager( int groupOffset, int topNGroups, boolean ignoreDocsWithoutGroupField) { + if (groupOffset < 0) { + throw new IllegalArgumentException("groupOffset must be >= 0 (got " + groupOffset + ")"); + } + if (topNGroups < 1) { + throw new IllegalArgumentException("topNGroups must be >= 1 (got " + topNGroups + ")"); + } this.groupSelectorFactory = groupSelectorFactory; this.groupSort = groupSort; this.groupOffset = groupOffset; diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index a01f5e0ce01d..7e58ed4ac230 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -279,7 +279,51 @@ public void testAllDocsWithoutGroupField() throws IOException { Collection> groups = searcher.search(MatchAllDocsQuery.INSTANCE, firstPassGroupingCollectorManager); - assertNull(groups); // Should return null when no groups found + assertNotNull(groups); + assertTrue(groups.isEmpty()); + + reader.close(); + dir.close(); + } + + public void testFirstPassGroupingCollectorManagerConstructor() { + IllegalArgumentException e1 = + expectThrows( + IllegalArgumentException.class, + () -> + new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector("group"), Sort.RELEVANCE, -1, 10)); + assertTrue(e1.getMessage().contains("groupOffset must be >= 0")); + + IllegalArgumentException e2 = + expectThrows( + IllegalArgumentException.class, + () -> + new FirstPassGroupingCollectorManager<>( + () -> new TermGroupSelector("group"), Sort.RELEVANCE, 0, 0)); + assertTrue(e2.getMessage().contains("topNGroups must be >= 1")); + } + + public void testFirstPassGroupingCollectorManagerReduceEmptyResult() throws IOException { + Directory dir = newDirectory(); + RandomIndexWriter w = + new RandomIndexWriter(random(), dir, newIndexWriterConfig(new MockAnalyzer(random()))); + Document doc = new Document(); + addGroupField(doc, "author", "author1"); + doc.add(new TextField("content", "random text", Field.Store.NO)); + w.addDocument(doc); + + DirectoryReader reader = w.getReader(); + w.close(); + IndexSearcher searcher = newSearcher(reader); + + // query matches nothing — reduce should return empty, not null + FirstPassGroupingCollectorManager manager = + createFirstPassCollectorManager(random().nextBoolean(), "author", Sort.RELEVANCE, 0, 10); + Collection result = + searcher.search(new TermQuery(new Term("content", "nonexistent")), manager); + assertNotNull(result); + assertTrue(result.isEmpty()); reader.close(); dir.close(); From 9503fe76333f28e91d1d2deaa3b9c6599ca23304 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Mon, 25 May 2026 16:20:39 +0800 Subject: [PATCH 08/10] Override more public search method in ShardSearcher Signed-off-by: Binlong Gao --- .../lucene/search/grouping/TestGrouping.java | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index 7e58ed4ac230..c025294926f8 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -58,6 +58,8 @@ import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TopFieldDocs; import org.apache.lucene.search.TotalHits; import org.apache.lucene.search.Weight; import org.apache.lucene.search.similarities.BM25Similarity; @@ -1667,7 +1669,7 @@ private TopGroups searchShards( docOffset + topNDocs, getMaxScores); final TopGroups topGroupsRaw = - subSearchers[shardIDX].search(w, secondPassCollectorManager); + subSearchers[shardIDX].search(query, secondPassCollectorManager); shardTopGroups.add(toByteTopGroups(topGroupsRaw)); if (VERBOSE) { System.out.println( @@ -1792,13 +1794,56 @@ public ShardSearcher(LeafReaderContext ctx, IndexReaderContext parent) { this.ctx = ctx; } - public T search(Weight weight, CollectorManager collectorManager) + @Override + public T search(Query query, CollectorManager collectorManager) throws IOException { + final Weight weight = createWeight(rewrite(query), ScoreMode.COMPLETE, 1); + return search(weight, collectorManager); + } + + private T search( + Weight weight, CollectorManager collectorManager) throws IOException { final C collector = collectorManager.newCollector(); searchLeaf(ctx, 0, DocIdSetIterator.NO_MORE_DOCS, weight, collector); return collectorManager.reduce(Collections.singletonList(collector)); } + @Override + public void search(Query query, Collector collector) { + throw new UnsupportedOperationException("Use search(Weight, CollectorManager) instead"); + } + + @Override + public TopDocs search(Query query, int n) { + throw new UnsupportedOperationException("Use search(Weight, CollectorManager) instead"); + } + + @Override + public TopFieldDocs search(Query query, int n, Sort sort) { + throw new UnsupportedOperationException("Use search(Weight, CollectorManager) instead"); + } + + @Override + public TopFieldDocs search(Query query, int n, Sort sort, boolean doDocScores) { + throw new UnsupportedOperationException("Use search(Weight, CollectorManager) instead"); + } + + @Override + public TopDocs searchAfter(ScoreDoc after, Query query, int n) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public TopDocs searchAfter(ScoreDoc after, Query query, int n, Sort sort) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public TopFieldDocs searchAfter( + ScoreDoc after, Query query, int n, Sort sort, boolean doDocScores) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public String toString() { return "ShardSearcher(" + ctx.reader() + ")"; From ad3daf4bfbf82f73377243c9cb0d970a8b41bd55 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Mon, 25 May 2026 16:36:29 +0800 Subject: [PATCH 09/10] Fix build failed Signed-off-by: Binlong Gao --- .../apache/lucene/search/grouping/TestGrouping.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index c025294926f8..0f8430cb5d14 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -1274,7 +1274,6 @@ public void testRandom() throws Exception { final TopGroups topGroupsShards = searchShards( - s, shards.subSearchers, query, groupSort, @@ -1483,7 +1482,6 @@ public void testRandom() throws Exception { // Get shard'd block grouping result: final TopGroups topGroupsBlockShards = searchShards( - sBlocks, shardsBlocks.subSearchers, query, groupSort, @@ -1565,7 +1563,6 @@ private void verifyShards(int[] docStarts, TopGroups topGroups) { } private TopGroups searchShards( - IndexSearcher topSearcher, ShardSearcher[] subSearchers, Query query, Sort groupSort, @@ -1592,13 +1589,6 @@ private TopGroups searchShards( + canUseIDV); } // Run 1st pass collector to get top groups per shard - final Weight w = - topSearcher.createWeight( - topSearcher.rewrite(query), - groupSort.needsScores() || docSort.needsScores() || getMaxScores - ? ScoreMode.COMPLETE - : ScoreMode.COMPLETE_NO_SCORES, - 1); final List>> shardGroups = new ArrayList<>(); String groupField = "group"; From 496e3dc639bbca746faf4539e008e82582b86553 Mon Sep 17 00:00:00 2001 From: Binlong Gao Date: Mon, 1 Jun 2026 17:47:05 +0800 Subject: [PATCH 10/10] Optimize code and fix issue Signed-off-by: Binlong Gao --- .../lucene/search/grouping/TestGrouping.java | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java index 0f8430cb5d14..b233f9c9ac45 100644 --- a/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java +++ b/lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java @@ -1607,27 +1607,25 @@ private TopGroups searchShards( final Collection> topGroups = toByteRefSearchGroups( subSearchers[shardIDX].search(query, firstPassGroupingCollectorManager)); - if (topGroups != null) { - if (VERBOSE) { + if (VERBOSE) { + System.out.println( + " shard " + + shardIDX + + " s=" + + subSearchers[shardIDX] + + " totalGroupedHitCount=?" + + " " + + topGroups.size() + + " groups:"); + for (SearchGroup group : topGroups) { System.out.println( - " shard " - + shardIDX - + " s=" - + subSearchers[shardIDX] - + " totalGroupedHitCount=?" - + " " - + topGroups.size() - + " groups:"); - for (SearchGroup group : topGroups) { - System.out.println( - " " - + groupToString(group.groupValue) - + " groupSort=" - + Arrays.toString(group.sortValues)); - } + " " + + groupToString(group.groupValue) + + " groupSort=" + + Arrays.toString(group.sortValues)); } - shardGroups.add(topGroups); } + shardGroups.add(topGroups); } final Collection> mergedTopGroups = @@ -1787,13 +1785,8 @@ public ShardSearcher(LeafReaderContext ctx, IndexReaderContext parent) { @Override public T search(Query query, CollectorManager collectorManager) throws IOException { - final Weight weight = createWeight(rewrite(query), ScoreMode.COMPLETE, 1); - return search(weight, collectorManager); - } - - private T search( - Weight weight, CollectorManager collectorManager) throws IOException { final C collector = collectorManager.newCollector(); + final Weight weight = createWeight(rewrite(query), collector.scoreMode(), 1); searchLeaf(ctx, 0, DocIdSetIterator.NO_MORE_DOCS, weight, collector); return collectorManager.reduce(Collections.singletonList(collector)); }