You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@lucenenet.apache.org by GitBox <gi...@apache.org> on 2021/04/02 21:38:09 UTC

[GitHub] [lucenenet] rclabo opened a new pull request #461: Fixed 1 GroupingSearch bug and added additional GroupingSearch tests to demonstrate usage

rclabo opened a new pull request #461:
URL: https://github.com/apache/lucenenet/pull/461


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [lucenenet] NightOwl888 commented on a change in pull request #461: Fixed 1 GroupingSearch bug and added additional GroupingSearch tests to demonstrate usage

Posted by GitBox <gi...@apache.org>.
NightOwl888 commented on a change in pull request #461:
URL: https://github.com/apache/lucenenet/pull/461#discussion_r606728901



##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";

Review comment:
       This test fails on Linux. `\r\n` is a Windows-only newline (see [In C#, what's the difference between \n and \r\n?](https://stackoverflow.com/a/3986129)). For this to be xplat, you will either need to use `Environment.NewLine` or use a different comparison method than strings.
   
   Actually, this would be more readable if the `expectedValue` were spread across multiple lines, anyway.
   
   

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+
+            */
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaName_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("major");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<BytesRef> topGroups = groupingSearch.Search<BytesRef>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    int val2 = NumericUtils.PrefixCodedToInt32(groupDocs.GroupValue);
+                    sb.AppendLine($"\r\nGroup: {val2}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("major").GetInt32Value()} {doc.GetField("minor").GetInt32Value()} {doc.GetField("rev").GetInt32Value()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: 1000\r\n1000 1102 21\r\n1000 1123 45\r\n\r\nGroup: 2000\r\n2000 2222 7\r\n2000 2888 88\r\n\r\nGroup: 3000\r\n3000 3123 11\r\n3000 3222 37\r\n3000 3993 9\r\n\r\nGroup: 4000\r\n4000 4001 88\r\n4000 4011 10\r\n\r\nGroup: 8000\r\n8000 8123 28\r\n8000 8888 8\r\n8000 8998 92\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+
+                Group: 1000
+                1000 1102 21
+                1000 1123 45
+
+                Group: 2000
+                2000 2222 7
+                2000 2888 88
+
+                Group: 3000
+                3000 3123 11
+                3000 3222 37
+                3000 3993 9
+
+                Group: 4000
+                4000 4001 88
+                4000 4011 10
+
+                Group: 8000
+                8000 8123 28
+                8000 8888 8
+                8000 8998 92
+            */
+        }
+
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by function/ValueSource/MutableValue approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaFunction_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            ValueSource vs = new BytesRefFieldSource("major");
+            GroupingSearch groupingSearch = new GroupingSearch(vs, new Hashtable());
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<MutableValue> groupDocs in topGroups.Groups)
+            {
+
+                if(groupDocs.GroupValue != null)
+                {
+                    BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;

Review comment:
       I believe there is a way to clean up all of this `object` and casting stuff so the end user doesn't have to deal with it.
   
   It would require the collectors to define a generic type so the user can pass the type of `MutableValue` they are dealing with. For example:
   
   ```c#
   public class FunctionFirstPassGroupingCollector<TMutableValue> : AbstractFirstPassGroupingCollector<TMutableValue>
       where TMutableValue : MutableValue
   ```
   
   #### Full Example
   
   <details>
     <summary>Click to expand!</summary>
   
   ```c#
       /// <summary>
       /// Concrete implementation of <see cref="AbstractFirstPassGroupingCollector{TGroupValue}"/> that groups based on
       /// <see cref="ValueSource"/> instances.
       /// 
       /// @lucene.experimental
       /// </summary>
       public class FunctionFirstPassGroupingCollector<TMutableValue> : AbstractFirstPassGroupingCollector<TMutableValue>
           where TMutableValue : MutableValue
       {
           private readonly ValueSource groupByVS;
           private readonly IDictionary /* Map<?, ?> */ vsContext;
   
           private FunctionValues.ValueFiller filler;
           private TMutableValue mval;
   
           /// <summary>
           /// Creates a first pass collector.
           /// </summary>
           /// <param name="groupByVS">The <see cref="ValueSource"/> instance to group by</param>
           /// <param name="vsContext">The <see cref="ValueSource"/> context</param>
           /// <param name="groupSort">
           /// The <see cref="Sort"/> used to sort the
           /// groups.  The top sorted document within each group
           /// according to groupSort, determines how that group
           /// sorts against other groups.  This must be non-null,
           /// ie, if you want to groupSort by relevance use
           /// <see cref="Sort.RELEVANCE"/>.
           /// </param>
           /// <param name="topNGroups">How many top groups to keep.</param>
           /// <exception cref="IOException">When I/O related errors occur</exception>
           public FunctionFirstPassGroupingCollector(ValueSource groupByVS, IDictionary /* Map<?, ?> */ vsContext, Sort groupSort, int topNGroups)
               : base(groupSort, topNGroups)
           {
               this.groupByVS = groupByVS;
               this.vsContext = vsContext;
           }
   
           protected override TMutableValue GetDocGroupValue(int doc)
           {
               filler.FillValue(doc);
               return mval;
           }
   
           protected override TMutableValue CopyDocGroupValue(TMutableValue groupValue, TMutableValue reuse)
           {
               if (reuse != null)
               {
                   reuse.Copy(groupValue);
                   return reuse;
               }
               return (TMutableValue)groupValue.Duplicate();
           }
   
           public override void SetNextReader(AtomicReaderContext context)
           {
               base.SetNextReader(context);
               FunctionValues values = groupByVS.GetValues(vsContext, context);
               filler = values.GetValueFiller();
               mval = (TMutableValue)filler.Value;
           }
       }
   ```
   
   </details>
   
   For this to fully work, the `GroupByFieldOrFunction` method would have to be subdivided into `GroupByField` and `GroupByFunction` which would allow the generic constraint `where TGroupValue : MutableValue` to be used on the `GroupByFunction` method and `where TGroupValue : Field` could be used on the `GroupByField` method.
   
   As long as we don't put constraints on the abstract classes, I believe this works for all subclasses so they can avoid end-user casting on custom types.
   
   These lines in the test could then be changed as follows:
   
   ```c#
   // Before
   ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
   // After
   ITopGroups<MutableValueStr> topGroups = groupingSearch.SearchByFunction<MutableValueStr>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
   ```
   
   > NOTE: I am showing this as if we were making 2 different functions, one with a generic constraint for `Field` and the other with a generic constraint for `MutableValue`. If this is broken into 2 classes (and I think that is probably best), we could move those generics up to the constructor of the class instead of putting them on every method call, and we wouldn't have to change the name from `Search` to `SearchByFunction`.
   
   ```c#
   // Before
   foreach (GroupDocs<MutableValue> groupDocs in topGroups.Groups)
   // After
   foreach (GroupDocs<MutableValueStr> groupDocs in topGroups.Groups)
   ```
   
   ```c#
   // Before
   BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;
   // After
   BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;
   ```
   
   I believe by first breaking the `GroupingSearch` class down into `GroupingFunctionSearch` and `GroupingFieldSearch` with a common generic abstraction, and then defining constraints on these 2 classes to expose the types as themselves instead of `MutableValue` and `Field` this would be possible.
   
   ```c#
   public class GroupingFunctionSearch<TMutableValue> where TMutableValue : MutableValue
   public class GroupingFieldSearch<TField> where TField : Field
   ```
   
   Once the classes are completely separated, then we could then use some conditional logic in `GroupingSearch` to work out which class to instantiate based on the type that is passed by the user.
   
   ```c#
   var groupValueType = typeof(TGroupValue);
   if (groupValueType.IsAssignableTo(typeof(MutableValue)))
       // Instantiate and execute search on GroupingFunctionSearch
   if (groupValueType.IsAssignableTo(typeof(Field))
       // Instantiate and execute search on GroupingFieldSearch
   ```
   
   If that bit doesn't work, worst case is that we just use the separate classes and drop the `GroupingSearch` class from the project. IMO it is doing way too much anyway, and it is the main reason why grouping has been such a chore to port.

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";

Review comment:
       This test fails on Linux. `\r\n` is a Windows-only newline (see [In C#, what's the difference between \n and \r\n?](https://stackoverflow.com/a/3986129)). For this to be xplat, you will either need to use `Environment.NewLine` or use a different comparison method than strings.
   
   Actually, this would be more readable if the `expectedValue` were spread across multiple lines, anyway.

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+
+            */
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaName_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("major");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<BytesRef> topGroups = groupingSearch.Search<BytesRef>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    int val2 = NumericUtils.PrefixCodedToInt32(groupDocs.GroupValue);
+                    sb.AppendLine($"\r\nGroup: {val2}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("major").GetInt32Value()} {doc.GetField("minor").GetInt32Value()} {doc.GetField("rev").GetInt32Value()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: 1000\r\n1000 1102 21\r\n1000 1123 45\r\n\r\nGroup: 2000\r\n2000 2222 7\r\n2000 2888 88\r\n\r\nGroup: 3000\r\n3000 3123 11\r\n3000 3222 37\r\n3000 3993 9\r\n\r\nGroup: 4000\r\n4000 4001 88\r\n4000 4011 10\r\n\r\nGroup: 8000\r\n8000 8123 28\r\n8000 8888 8\r\n8000 8998 92\r\n";

Review comment:
       This test fails on Linux. `\r\n` is a Windows-only newline (see [In C#, what's the difference between \n and \r\n?](https://stackoverflow.com/a/3986129)). For this to be xplat, you will either need to use `Environment.NewLine` or use a different comparison method than strings.
   
   Actually, this would be more readable if the `expectedValue` were spread across multiple lines, anyway.

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]

Review comment:
       Please add the `[LuceneNetSpecific]` attribute to this test (or the class).

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]

Review comment:
       Please add the `[LuceneNetSpecific]` attribute to this test (or the class).

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+
+            */
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]

Review comment:
       Please add the `[LuceneNetSpecific]` attribute to this test (or the class).

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+
+            */
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaName_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("major");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<BytesRef> topGroups = groupingSearch.Search<BytesRef>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    int val2 = NumericUtils.PrefixCodedToInt32(groupDocs.GroupValue);
+                    sb.AppendLine($"\r\nGroup: {val2}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("major").GetInt32Value()} {doc.GetField("minor").GetInt32Value()} {doc.GetField("rev").GetInt32Value()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: 1000\r\n1000 1102 21\r\n1000 1123 45\r\n\r\nGroup: 2000\r\n2000 2222 7\r\n2000 2888 88\r\n\r\nGroup: 3000\r\n3000 3123 11\r\n3000 3222 37\r\n3000 3993 9\r\n\r\nGroup: 4000\r\n4000 4001 88\r\n4000 4011 10\r\n\r\nGroup: 8000\r\n8000 8123 28\r\n8000 8888 8\r\n8000 8998 92\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+
+                Group: 1000
+                1000 1102 21
+                1000 1123 45
+
+                Group: 2000
+                2000 2222 7
+                2000 2888 88
+
+                Group: 3000
+                3000 3123 11
+                3000 3222 37
+                3000 3993 9
+
+                Group: 4000
+                4000 4001 88
+                4000 4011 10
+
+                Group: 8000
+                8000 8123 28
+                8000 8888 8
+                8000 8998 92
+            */
+        }
+
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by function/ValueSource/MutableValue approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]

Review comment:
       Please add the `[LuceneNetSpecific]` attribute to this test (or the class).

##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+
+            */
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaName_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("major");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<BytesRef> topGroups = groupingSearch.Search<BytesRef>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    int val2 = NumericUtils.PrefixCodedToInt32(groupDocs.GroupValue);
+                    sb.AppendLine($"\r\nGroup: {val2}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("major").GetInt32Value()} {doc.GetField("minor").GetInt32Value()} {doc.GetField("rev").GetInt32Value()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: 1000\r\n1000 1102 21\r\n1000 1123 45\r\n\r\nGroup: 2000\r\n2000 2222 7\r\n2000 2888 88\r\n\r\nGroup: 3000\r\n3000 3123 11\r\n3000 3222 37\r\n3000 3993 9\r\n\r\nGroup: 4000\r\n4000 4001 88\r\n4000 4011 10\r\n\r\nGroup: 8000\r\n8000 8123 28\r\n8000 8888 8\r\n8000 8998 92\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+
+                Group: 1000
+                1000 1102 21
+                1000 1123 45
+
+                Group: 2000
+                2000 2222 7
+                2000 2888 88
+
+                Group: 3000
+                3000 3123 11
+                3000 3222 37
+                3000 3993 9
+
+                Group: 4000
+                4000 4001 88
+                4000 4011 10
+
+                Group: 8000
+                8000 8123 28
+                8000 8888 8
+                8000 8998 92
+            */
+        }
+
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by function/ValueSource/MutableValue approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaFunction_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            ValueSource vs = new BytesRefFieldSource("major");
+            GroupingSearch groupingSearch = new GroupingSearch(vs, new Hashtable());
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<MutableValue> groupDocs in topGroups.Groups)
+            {
+
+                if(groupDocs.GroupValue != null)
+                {
+                    BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;
+                    int major = NumericUtils.PrefixCodedToInt32(bytesRef);
+                    sb.AppendLine($"\r\nGroup: {major}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("major").GetInt32Value()} {doc.GetField("minor").GetInt32Value()} {doc.GetField("rev").GetInt32Value()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: 1000\r\n1000 1102 21\r\n1000 1123 45\r\n\r\nGroup: 2000\r\n2000 2222 7\r\n2000 2888 88\r\n\r\nGroup: 3000\r\n3000 3123 11\r\n3000 3222 37\r\n3000 3993 9\r\n\r\nGroup: 4000\r\n4000 4001 88\r\n4000 4011 10\r\n\r\nGroup: 8000\r\n8000 8123 28\r\n8000 8888 8\r\n8000 8998 92\r\n";

Review comment:
       This test fails on Linux. `\r\n` is a Windows-only newline (see [In C#, what's the difference between \n and \r\n?](https://stackoverflow.com/a/3986129)). For this to be xplat, you will either need to use `Environment.NewLine` or use a different comparison method than strings.
   
   Actually, this would be more readable if the `expectedValue` were spread across multiple lines, anyway.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [lucenenet] NightOwl888 merged pull request #461: Fixed 1 GroupingSearch bug and added additional GroupingSearch tests to demonstrate usage

Posted by GitBox <gi...@apache.org>.
NightOwl888 merged pull request #461:
URL: https://github.com/apache/lucenenet/pull/461


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [lucenenet] NightOwl888 commented on a change in pull request #461: Fixed 1 GroupingSearch bug and added additional GroupingSearch tests to demonstrate usage

Posted by GitBox <gi...@apache.org>.
NightOwl888 commented on a change in pull request #461:
URL: https://github.com/apache/lucenenet/pull/461#discussion_r606729357



##########
File path: src/Lucene.Net.Tests.Grouping/TestGroupingExtra.cs
##########
@@ -0,0 +1,503 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries.Function;
+using Lucene.Net.Queries.Function.ValueSources;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Lucene.Net.Util.Mutable;
+using NUnit.Framework;
+using System.Collections;
+using System.Text;
+
+namespace Lucene.Net.Search.Grouping
+{
+
+    /// <summary>
+    /// LUCENENET: File not includes in java Lucene. This file contains extra
+    /// tests to test a few specific ways of using grouping.  
+    /// </summary>
+    public class TestGroupingExtra : LuceneTestCase
+    {
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingFieldCache_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+            */
+
+
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by a StringField via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public void GroupingSearch_ViaName_StringSorted_UsingDocValues_Top3Groups_Top4DocsEach()
+        {
+            string[,] carData = GetCarData();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            int carCount = carData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < carCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new StringField("carMake", carData[i, 0], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carMake_dv", new BytesRef(carData[i, 0])));
+                doc.Add(new StringField("carModel", carData[i, 1], Field.Store.YES));
+                doc.Add(new SortedDocValuesField("carModel_dv", new BytesRef(carData[i, 1])));
+                doc.Add(new StringField("carColor", carData[i, 2], Field.Store.YES));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("carMake");
+            groupingSearch.SetAllGroups(true);                    //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(4);                 //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("carMake_dv", SortFieldType.STRING)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("carModel_dv", SortFieldType.STRING)));
+            groupingSearch.SetFillSortFields(true);
+            groupingSearch.SetCachingInMB(10, cacheScores: true);
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 3);
+
+
+            int? totalGroupCount = topGroups.TotalGroupCount;               //null if not computed
+            int totalGroupedHitCount = topGroups.TotalGroupedHitCount;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    sb.AppendLine($"\r\nGroup: {groupDocs.GroupValue.Utf8ToString()}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("carMake").GetStringValue()} {doc.GetField("carModel").GetStringValue()} {doc.GetField("carColor").GetStringValue()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: Audi\r\nAudi A3 Orange\r\nAudi A3 Green\r\nAudi A3 Blue\r\nAudi S4 Yellow\r\n\r\nGroup: Bently\r\nBently Arnage Grey\r\nBently Arnage Blue\r\nBently Azure Green\r\nBently Azure Blue\r\n\r\nGroup: Ford\r\nFord Aspire Yellow\r\nFord Aspire Blue\r\nFord Bronco Green\r\nFord Bronco Orange\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+             
+                Group: Audi
+                Audi A3 Orange
+                Audi A3 Green
+                Audi A3 Blue
+                Audi S4 Yellow
+
+                Group: Bently
+                Bently Arnage Grey
+                Bently Arnage Blue
+                Bently Azure Green
+                Bently Azure Blue
+
+                Group: Ford
+                Ford Aspire Yellow
+                Ford Aspire Blue
+                Ford Bronco Green
+                Ford Bronco Orange
+
+            */
+        }
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by field name approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaName_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            GroupingSearch groupingSearch = new GroupingSearch("major");
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<BytesRef> topGroups = groupingSearch.Search<BytesRef>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<BytesRef> groupDocs in topGroups.Groups)
+            {
+                if (groupDocs.GroupValue != null)
+                {
+                    int val2 = NumericUtils.PrefixCodedToInt32(groupDocs.GroupValue);
+                    sb.AppendLine($"\r\nGroup: {val2}");
+                }
+                else
+                {
+                    sb.AppendLine($"\r\nUngrouped");    //Happens when matching documents don't contain the group field
+                }
+
+                foreach (ScoreDoc scoreDoc in groupDocs.ScoreDocs)
+                {
+                    doc = searcher.Doc(scoreDoc.Doc);
+                    sb.AppendLine($"{doc.GetField("major").GetInt32Value()} {doc.GetField("minor").GetInt32Value()} {doc.GetField("rev").GetInt32Value()}");
+                }
+            }
+
+            string output = sb.ToString();
+            string expectdValue = "\r\nGroup: 1000\r\n1000 1102 21\r\n1000 1123 45\r\n\r\nGroup: 2000\r\n2000 2222 7\r\n2000 2888 88\r\n\r\nGroup: 3000\r\n3000 3123 11\r\n3000 3222 37\r\n3000 3993 9\r\n\r\nGroup: 4000\r\n4000 4001 88\r\n4000 4011 10\r\n\r\nGroup: 8000\r\n8000 8123 28\r\n8000 8888 8\r\n8000 8998 92\r\n";
+            assertEquals(expectdValue, output);
+
+            /*  Output:
+
+                Group: 1000
+                1000 1102 21
+                1000 1123 45
+
+                Group: 2000
+                2000 2222 7
+                2000 2888 88
+
+                Group: 3000
+                3000 3123 11
+                3000 3222 37
+                3000 3993 9
+
+                Group: 4000
+                4000 4001 88
+                4000 4011 10
+
+                Group: 8000
+                8000 8123 28
+                8000 8888 8
+                8000 8998 92
+            */
+        }
+
+
+        /// <summary>
+        /// LUCENENET: Additional Unit Test.  Tests grouping by an Int32 via the
+        /// 2 pass by function/ValueSource/MutableValue approach. Uses FieldCache, not DocValues.
+        /// </summary>
+        [Test]
+        public virtual void GroupingSearch_ViaFunction_Int32Sorted_UsingFieldCache_Top10Groups_Top10DocsEach()
+        {
+            int[,] numericData = GetNumbers();
+
+            Directory indexDir = NewDirectory();
+            Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
+
+            IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
+            IndexWriter writer = new IndexWriter(indexDir, indexConfig);
+
+
+            //Normally we can not group on a Int32Field because it's stored as a 8 term trie structure
+            //by default.  But by specifying int.MaxValue as the NumericPrecisionStep we force the inverted
+            //index to store the value as a single term. This allows us to use it for grouping (although
+            //it's no longer good for range queries as they will be slow if the range is large). 
+
+            var int32OneTerm = new FieldType
+            {
+                IsIndexed = true,
+                IsTokenized = true,
+                OmitNorms = true,
+                IndexOptions = IndexOptions.DOCS_ONLY,
+                NumericType = Documents.NumericType.INT32,
+                NumericPrecisionStep = int.MaxValue,             //Ensures a single term is generated not a trie
+                IsStored = true
+            };
+            int32OneTerm.Freeze();
+
+            int rowCount = numericData.GetLength(0);
+            Document doc = new Document();
+            for (int i = 0; i < rowCount; i++)
+            {
+                doc.Fields.Clear();
+                doc.Add(new Int32Field("major", numericData[i, 0], int32OneTerm));
+                doc.Add(new Int32Field("minor", numericData[i, 1], int32OneTerm));
+                doc.Add(new StoredField("rev", numericData[i, 2]));
+                writer.AddDocument(doc);
+            }
+            writer.Commit();
+
+            ValueSource vs = new BytesRefFieldSource("major");
+            GroupingSearch groupingSearch = new GroupingSearch(vs, new Hashtable());
+            groupingSearch.SetAllGroups(true);                      //true = compute all groups matching the query
+            groupingSearch.SetGroupDocsLimit(10);                   //max docs returned in a group
+            groupingSearch.SetGroupSort(new Sort(new SortField("major", SortFieldType.INT32)));
+            groupingSearch.SetSortWithinGroup(new Sort(new SortField("minor", SortFieldType.INT32)));
+
+            IndexReader reader = writer.GetReader(applyAllDeletes: true);
+            IndexSearcher searcher = new IndexSearcher(reader);
+            Query matchAllQuery = new MatchAllDocsQuery();
+            ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
+
+            var val = FieldCache.DEFAULT;
+
+            StringBuilder sb = new StringBuilder();
+            foreach (GroupDocs<MutableValue> groupDocs in topGroups.Groups)
+            {
+
+                if(groupDocs.GroupValue != null)
+                {
+                    BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;

Review comment:
       I believe there is a way to clean up all of this `object` and casting stuff so the end user doesn't have to deal with it.
   
   It would require the collectors to define a generic type so the user can pass the type of `MutableValue` they are dealing with. For example:
   
   ```c#
   public class FunctionFirstPassGroupingCollector<TMutableValue> : AbstractFirstPassGroupingCollector<TMutableValue>
       where TMutableValue : MutableValue
   ```
   
   #### Full Example
   
   <details>
     <summary>Click to expand!</summary>
   
   ```c#
       /// <summary>
       /// Concrete implementation of <see cref="AbstractFirstPassGroupingCollector{TGroupValue}"/> that groups based on
       /// <see cref="ValueSource"/> instances.
       /// 
       /// @lucene.experimental
       /// </summary>
       public class FunctionFirstPassGroupingCollector<TMutableValue> : AbstractFirstPassGroupingCollector<TMutableValue>
           where TMutableValue : MutableValue
       {
           private readonly ValueSource groupByVS;
           private readonly IDictionary /* Map<?, ?> */ vsContext;
   
           private FunctionValues.ValueFiller filler;
           private TMutableValue mval;
   
           /// <summary>
           /// Creates a first pass collector.
           /// </summary>
           /// <param name="groupByVS">The <see cref="ValueSource"/> instance to group by</param>
           /// <param name="vsContext">The <see cref="ValueSource"/> context</param>
           /// <param name="groupSort">
           /// The <see cref="Sort"/> used to sort the
           /// groups.  The top sorted document within each group
           /// according to groupSort, determines how that group
           /// sorts against other groups.  This must be non-null,
           /// ie, if you want to groupSort by relevance use
           /// <see cref="Sort.RELEVANCE"/>.
           /// </param>
           /// <param name="topNGroups">How many top groups to keep.</param>
           /// <exception cref="IOException">When I/O related errors occur</exception>
           public FunctionFirstPassGroupingCollector(ValueSource groupByVS, IDictionary /* Map<?, ?> */ vsContext, Sort groupSort, int topNGroups)
               : base(groupSort, topNGroups)
           {
               this.groupByVS = groupByVS;
               this.vsContext = vsContext;
           }
   
           protected override TMutableValue GetDocGroupValue(int doc)
           {
               filler.FillValue(doc);
               return mval;
           }
   
           protected override TMutableValue CopyDocGroupValue(TMutableValue groupValue, TMutableValue reuse)
           {
               if (reuse != null)
               {
                   reuse.Copy(groupValue);
                   return reuse;
               }
               return (TMutableValue)groupValue.Duplicate();
           }
   
           public override void SetNextReader(AtomicReaderContext context)
           {
               base.SetNextReader(context);
               FunctionValues values = groupByVS.GetValues(vsContext, context);
               filler = values.GetValueFiller();
               mval = (TMutableValue)filler.Value;
           }
       }
   ```
   
   </details>
   
   For this to fully work, the `GroupByFieldOrFunction` method would have to be subdivided into `GroupByField` and `GroupByFunction` which would allow the generic constraint `where TGroupValue : MutableValue` to be used on the `GroupByFunction` method and `where TGroupValue : Field` could be used on the `GroupByField` method.
   
   As long as we don't put constraints on the abstract classes, I believe this works for all subclasses so they can avoid end-user casting on custom types.
   
   These lines in the test could then be changed as follows:
   
   ```c#
   // Before
   ITopGroups<object> topGroups = groupingSearch.Search(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
   // After
   ITopGroups<MutableValueStr> topGroups = groupingSearch.SearchByFunction<MutableValueStr>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
   ```
   
   > NOTE: I am showing this as if we were making 2 different functions, one with a generic constraint for `Field` and the other with a generic constraint for `MutableValue`. If this is broken into 2 classes (and I think that is probably best), we could move those generics up to the constructor of the class instead of putting them on every method call, and we wouldn't have to change the name from `Search` to `SearchByFunction`.
   
   ```c#
   // Before
   foreach (GroupDocs<MutableValue> groupDocs in topGroups.Groups)
   // After
   foreach (GroupDocs<MutableValueStr> groupDocs in topGroups.Groups)
   ```
   
   ```c#
   // Before
   BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;
   // After
   BytesRef bytesRef = ((MutableValueStr)groupDocs.GroupValue).Value;
   ```
   
   I believe by first breaking the `GroupingSearch` class down into `GroupingFunctionSearch` and `GroupingFieldSearch` with a common generic abstraction, and then defining constraints on these 2 classes to expose the types as themselves instead of `MutableValue` and `Field` this would be possible.
   
   ```c#
   public class GroupingFunctionSearch<TMutableValue> where TMutableValue : MutableValue
   public class GroupingFieldSearch<TField> where TField : Field
   ```
   
   Once the classes are completely separated, then we could then use some conditional logic in `GroupingSearch` to work out which class to instantiate based on the type that is passed by the user.
   
   ```c#
   var groupValueType = typeof(TGroupValue);
   if (groupValueType.IsAssignableTo(typeof(MutableValue)))
       // Instantiate and execute search on GroupingFunctionSearch
   if (groupValueType.IsAssignableTo(typeof(Field))
       // Instantiate and execute search on GroupingFieldSearch
   ```
   
   If that bit doesn't work, chances are we are going to need to replace the `Search` method with a `SearchByFunction` method and a `SearchByField` method so a different generic constraint can be applied to each method in .NET. I don't think there is a way to make a "switchable" constraint. But if separating these responsibilities can save the user from having to discover the types that they are dealing with and cast to use the API is worth diverging from Lucene IMO.
   
   ```c#
    // Usage example after changing method constraint to "where TMutableValue : MutableValue"
   ITopGroups<MutableValueStr> topGroups = groupingSearch.SearchByFunction<MutableValueStr>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
    // Usage example after changing method constraint to "where TField : Field"
   ITopGroups<StringField> topGroups = groupingSearch.SearchByField<StringField>(searcher, matchAllQuery, groupOffset: 0, groupLimit: 10);
   ```
   
   The compiler will then enforce the fact that the type must either derive from `MutableValue` or `Field` which makes the developer experience better because we get compile time checking instead of trying to reason out why our casting isn't working at runtime.
   
   And it would be even a better experience if the user doesn't use `GroupingSearch` and instead uses either `GroupingFunctionSearch<TMutableValue>` or `GroupingFieldSearch<TField>` so they can define the type that they want to deal with at the class level instead of every search call.
   
   ```c#
   public class GroupingFunctionSearch<TMutableValue> where TMutableValue : MutableValue
   public class GroupingFieldSearch<TField> where TField : Field
   ```
   
   > I know you mentioned that there are 3 types of search in `GroupingSearch`. I suspect that the 3rd type could be done in a similar way.
   
   Once we try that, it is worth stepping back and seeing if we can eliminate the need for covariance and the extra interfaces that were added to support it so we can go back to using `ICollection<T>` like Lucene did.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org