You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by ni...@apache.org on 2016/11/10 11:33:51 UTC

[40/58] [abbrv] lucenenet git commit: Added interfaces to GroupDocs, SearchGroup, and TopGroups to apply covariance so they act more like the wildcard generics that were used in Java.

Added interfaces to GroupDocs, SearchGroup, and TopGroups to apply covariance so they act more like the wildcard generics that were used in Java.


Project: http://git-wip-us.apache.org/repos/asf/lucenenet/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucenenet/commit/0a137bea
Tree: http://git-wip-us.apache.org/repos/asf/lucenenet/tree/0a137bea
Diff: http://git-wip-us.apache.org/repos/asf/lucenenet/diff/0a137bea

Branch: refs/heads/grouping
Commit: 0a137bea5285ac0022861b67339665ace2ae9f49
Parents: a8f7f42
Author: Shad Storhaug <sh...@shadstorhaug.com>
Authored: Wed Nov 2 20:37:20 2016 +0700
Committer: Shad Storhaug <sh...@shadstorhaug.com>
Committed: Tue Nov 8 02:24:55 2016 +0700

----------------------------------------------------------------------
 .../AbstractAllGroupHeadsCollector.cs           |  42 +--
 .../AbstractAllGroupsCollector.cs               |  65 +++--
 .../AbstractFirstPassGroupingCollector.cs       |  41 ++-
 .../AbstractSecondPassGroupingCollector.cs      |  25 +-
 .../Function/FunctionAllGroupsCollector.cs      |   2 +-
 .../Function/FunctionDistinctValuesCollector.cs |   2 +-
 src/Lucene.Net.Grouping/GroupDocs.cs            |  57 +++-
 src/Lucene.Net.Grouping/GroupingSearch.cs       | 286 ++++++++++---------
 src/Lucene.Net.Grouping/SearchGroup.cs          |  47 +--
 .../Term/TermAllGroupsCollector.cs              |   2 +-
 .../Term/TermDistinctValuesCollector.cs         |   2 +-
 src/Lucene.Net.Grouping/TopGroups.cs            |  62 +++-
 12 files changed, 392 insertions(+), 241 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/AbstractAllGroupHeadsCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/AbstractAllGroupHeadsCollector.cs b/src/Lucene.Net.Grouping/AbstractAllGroupHeadsCollector.cs
index a3ae1bf..f27d2e4 100644
--- a/src/Lucene.Net.Grouping/AbstractAllGroupHeadsCollector.cs
+++ b/src/Lucene.Net.Grouping/AbstractAllGroupHeadsCollector.cs
@@ -224,22 +224,28 @@ namespace Lucene.Net.Search.Grouping
         protected abstract void RetrieveGroupHeadAndAddIfNotExist(int doc);
     }
 
-    /////// <summary>
-    /////// LUCENENET specific interface used to reference an 
-    /////// <see cref="AbstractAllGroupHeadsCollector{GH}"/> subclass
-    /////// without refering to its generic closing type.
-    /////// </summary>
-    ////public interface IAllGroupHeadsCollector
-    ////{
-    ////    // From AbstractAllGroupHeadsCollector{GH}
-    ////    FixedBitSet RetrieveGroupHeads(int maxDoc);
-    ////    int[] RetrieveGroupHeads();
-    ////    int GroupHeadsSize { get; }
-    ////    void Collect(int doc);
-    ////    bool AcceptsDocsOutOfOrder();
-
-    ////    // From Collector
-    ////    Scorer Scorer { set; }
-    ////    AtomicReaderContext NextReader { set; }
-    ////}
+    ///// <summary>
+    ///// LUCENENET specific interface used to apply covariance to GH
+    ///// </summary>
+    //public interface IAbstractAllGroupHeadsCollector<out GH>
+    //{
+    //    /// <summary>
+    //    /// 
+    //    /// </summary>
+    //    /// <param name="maxDoc">The maxDoc of the top level <see cref="Index.IndexReader"/></param>
+    //    /// <returns>a <see cref="FixedBitSet"/> containing all group heads.</returns>
+    //    FixedBitSet RetrieveGroupHeads(int maxDoc);
+
+    //    /// <summary>
+    //    /// 
+    //    /// </summary>
+    //    /// <returns>an int array containing all group heads. The size of the array is equal to number of collected unique groups.</returns>
+    //    int[] RetrieveGroupHeads();
+
+    //    /// <summary>
+    //    /// 
+    //    /// </summary>
+    //    /// <returns>the number of group heads found for a query.</returns>
+    //    int GroupHeadsSize { get; }
+    //}
 }

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/AbstractAllGroupsCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/AbstractAllGroupsCollector.cs b/src/Lucene.Net.Grouping/AbstractAllGroupsCollector.cs
index 0c8bc14..df240ee 100644
--- a/src/Lucene.Net.Grouping/AbstractAllGroupsCollector.cs
+++ b/src/Lucene.Net.Grouping/AbstractAllGroupsCollector.cs
@@ -1,4 +1,5 @@
 \ufeffusing System.Collections.Generic;
+using System.Linq;
 
 namespace Lucene.Net.Search.Grouping
 {
@@ -15,18 +16,18 @@ namespace Lucene.Net.Search.Grouping
     /// @lucene.experimental
     /// </summary>
     /// <typeparam name="TGroupValue"></typeparam>
-    public abstract class AbstractAllGroupsCollector<TGroupValue> : AbstractAllGroupsCollector
+    public abstract class AbstractAllGroupsCollector<TGroupValue> : Collector, IAbstractAllGroupsCollector<TGroupValue>
     {
         /// <summary>
         /// Returns the total number of groups for the executed search.
         /// This is a convenience method. The following code snippet has the same effect: <code>GetGroups().Count</code>
         /// </summary>
         /// <returns>The total number of groups for the executed search</returns>
-        public override int GroupCount
+        public virtual int GroupCount
         {
             get
             {
-                return Groups.Count;
+                return Groups.Count();
             }
         }
 
@@ -38,7 +39,7 @@ namespace Lucene.Net.Search.Grouping
         /// </para>
         /// </summary>
         /// <returns>the group values</returns>
-        public abstract ICollection<TGroupValue> Groups { get; }
+        public abstract IEnumerable<TGroupValue> Groups { get; }
 
 
         // Empty not necessary
@@ -56,30 +57,54 @@ namespace Lucene.Net.Search.Grouping
     }
 
     /// <summary>
-    /// LUCENENET specific class used to reference <see cref="AbstractAllGroupsCollector{TGroupValue}"/>
-    /// without refering to its generic closing type.
+    /// LUCENENET specific interface used to apply covariance to TGroupValue
     /// </summary>
-    public abstract class AbstractAllGroupsCollector : Collector
+    /// <typeparam name="TGroupValue"></typeparam>
+    public interface IAbstractAllGroupsCollector<out TGroupValue>
     {
         /// <summary>
         /// Returns the total number of groups for the executed search.
         /// This is a convenience method. The following code snippet has the same effect: <code>GetGroups().Count</code>
         /// </summary>
         /// <returns>The total number of groups for the executed search</returns>
-        public abstract int GroupCount { get; }
+        int GroupCount { get; }
 
+        /// <summary>
+        /// Returns the group values
+        /// <para>
+        /// This is an unordered collections of group values. For each group that matched the query there is a <see cref="BytesRef"/>
+        /// representing a group value.
+        /// </para>
+        /// </summary>
+        /// <returns>the group values</returns>
+        IEnumerable<TGroupValue> Groups { get; }
+    }
 
-        // Empty not necessary
-        public override Scorer Scorer
-        {
-            set
-            {
-            }
-        }
+    ///// <summary>
+    ///// LUCENENET specific class used to reference <see cref="AbstractAllGroupsCollector{TGroupValue}"/>
+    ///// without refering to its generic closing type.
+    ///// </summary>
+    //public abstract class AbstractAllGroupsCollector : Collector
+    //{
+    //    /// <summary>
+    //    /// Returns the total number of groups for the executed search.
+    //    /// This is a convenience method. The following code snippet has the same effect: <code>GetGroups().Count</code>
+    //    /// </summary>
+    //    /// <returns>The total number of groups for the executed search</returns>
+    //    public abstract int GroupCount { get; }
 
-        public override bool AcceptsDocsOutOfOrder()
-        {
-            return true;
-        }
-    }
+
+    //    // Empty not necessary
+    //    public override Scorer Scorer
+    //    {
+    //        set
+    //        {
+    //        }
+    //    }
+
+    //    public override bool AcceptsDocsOutOfOrder()
+    //    {
+    //        return true;
+    //    }
+    //}
 }

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/AbstractFirstPassGroupingCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/AbstractFirstPassGroupingCollector.cs b/src/Lucene.Net.Grouping/AbstractFirstPassGroupingCollector.cs
index 8efafd2..0fb682d 100644
--- a/src/Lucene.Net.Grouping/AbstractFirstPassGroupingCollector.cs
+++ b/src/Lucene.Net.Grouping/AbstractFirstPassGroupingCollector.cs
@@ -22,7 +22,7 @@ namespace Lucene.Net.Search.Grouping
     /// @lucene.experimental
     /// </summary>
     /// <typeparam name="TGroupValue"></typeparam>
-    public abstract class AbstractFirstPassGroupingCollector<TGroupValue> : Collector
+    public abstract class AbstractFirstPassGroupingCollector<TGroupValue> : Collector, IAbstractFirstPassGroupingCollector<TGroupValue>
     {
         private readonly Sort groupSort;
         private readonly FieldComparator[] comparators;
@@ -88,7 +88,7 @@ namespace Lucene.Net.Search.Grouping
         /// <param name="groupOffset">The offset in the collected groups</param>
         /// <param name="fillFields">Whether to fill to <see cref="SearchGroup.sortValues"/></param>
         /// <returns>top groups, starting from offset</returns>
-        public ICollection<SearchGroup<TGroupValue>> GetTopGroups(int groupOffset, bool fillFields)
+        public IEnumerable<ISearchGroup<TGroupValue>> GetTopGroups(int groupOffset, bool fillFields)
         {
 
             //System.out.println("FP.getTopGroups groupOffset=" + groupOffset + " fillFields=" + fillFields + " groupMap.size()=" + groupMap.size());
@@ -108,7 +108,7 @@ namespace Lucene.Net.Search.Grouping
                 BuildSortedSet();
             }
 
-            ICollection<SearchGroup<TGroupValue>> result = new List<SearchGroup<TGroupValue>>();
+            ICollection<ISearchGroup<TGroupValue>> result = new List<ISearchGroup<TGroupValue>>();
             int upto = 0;
             int sortFieldCount = groupSort.GetSort().Length;
             foreach (CollectedSearchGroup<TGroupValue> group in orderedGroups)
@@ -119,13 +119,13 @@ namespace Lucene.Net.Search.Grouping
                 }
                 //System.out.println("  group=" + (group.groupValue == null ? "null" : group.groupValue.utf8ToString()));
                 SearchGroup<TGroupValue> searchGroup = new SearchGroup<TGroupValue>();
-                searchGroup.groupValue = group.groupValue;
+                searchGroup.GroupValue = group.GroupValue;
                 if (fillFields)
                 {
-                    searchGroup.sortValues = new object[sortFieldCount];
+                    searchGroup.SortValues = new object[sortFieldCount];
                     for (int sortFieldIDX = 0; sortFieldIDX < sortFieldCount; sortFieldIDX++)
                     {
-                        searchGroup.sortValues[sortFieldIDX] = comparators[sortFieldIDX].Value(group.ComparatorSlot);
+                        searchGroup.SortValues[sortFieldIDX] = comparators[sortFieldIDX].Value(group.ComparatorSlot);
                     }
                 }
                 result.Add(searchGroup);
@@ -206,14 +206,14 @@ namespace Lucene.Net.Search.Grouping
 
                     // Add a new CollectedSearchGroup:
                     CollectedSearchGroup<TGroupValue> sg = new CollectedSearchGroup<TGroupValue>();
-                    sg.groupValue = CopyDocGroupValue(groupValue, default(TGroupValue));
+                    sg.GroupValue = CopyDocGroupValue(groupValue, default(TGroupValue));
                     sg.ComparatorSlot = groupMap.Count;
                     sg.TopDoc = docBase + doc;
                     foreach (FieldComparator fc in comparators)
                     {
                         fc.Copy(sg.ComparatorSlot, doc);
                     }
-                    groupMap[sg.groupValue] = sg;
+                    groupMap[sg.GroupValue] = sg;
 
                     if (groupMap.Count == topNGroups)
                     {
@@ -237,10 +237,10 @@ namespace Lucene.Net.Search.Grouping
                 }
                 Debug.Assert(orderedGroups.Count == topNGroups - 1);
 
-                groupMap.Remove(bottomGroup.groupValue);
+                groupMap.Remove(bottomGroup.GroupValue);
 
                 // reuse the removed CollectedSearchGroup
-                bottomGroup.groupValue = CopyDocGroupValue(groupValue, bottomGroup.groupValue);
+                bottomGroup.GroupValue = CopyDocGroupValue(groupValue, bottomGroup.GroupValue);
                 bottomGroup.TopDoc = docBase + doc;
 
                 foreach (FieldComparator fc in comparators)
@@ -248,7 +248,7 @@ namespace Lucene.Net.Search.Grouping
                     fc.Copy(bottomGroup.ComparatorSlot, doc);
                 }
 
-                groupMap[bottomGroup.groupValue] = bottomGroup;
+                groupMap[bottomGroup.GroupValue] = bottomGroup;
                 orderedGroups.Add(bottomGroup);
                 Debug.Assert(orderedGroups.Count == topNGroups);
 
@@ -424,4 +424,23 @@ namespace Lucene.Net.Search.Grouping
         protected abstract TGroupValue CopyDocGroupValue(TGroupValue groupValue, TGroupValue reuse);
 
     }
+
+    /// <summary>
+    /// LUCENENET specific interface used to apply covariance to TGroupValue
+    /// </summary>
+    /// <typeparam name="TGroupValue"></typeparam>
+    public interface IAbstractFirstPassGroupingCollector<out TGroupValue>
+    {
+        // LUCENENET NOTE: We must use IEnumerable rather than ICollection here because we need
+        // this to be covariant
+        /// <summary>
+        /// Returns top groups, starting from offset.  This may
+        /// return null, if no groups were collected, or if the
+        /// number of unique groups collected is &lt;= offset.
+        /// </summary>
+        /// <param name="groupOffset">The offset in the collected groups</param>
+        /// <param name="fillFields">Whether to fill to <see cref="SearchGroup.sortValues"/></param>
+        /// <returns>top groups, starting from offset</returns>
+        IEnumerable<ISearchGroup<TGroupValue>> GetTopGroups(int groupOffset, bool fillFields);
+    }
 }

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/AbstractSecondPassGroupingCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/AbstractSecondPassGroupingCollector.cs b/src/Lucene.Net.Grouping/AbstractSecondPassGroupingCollector.cs
index 580617c..41b5837 100644
--- a/src/Lucene.Net.Grouping/AbstractSecondPassGroupingCollector.cs
+++ b/src/Lucene.Net.Grouping/AbstractSecondPassGroupingCollector.cs
@@ -26,19 +26,19 @@ namespace Lucene.Net.Search.Grouping
         protected readonly IDictionary<TGroupValue, AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue>> groupMap;
         private readonly int maxDocsPerGroup;
         protected AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue>[] groupDocs;
-        private readonly ICollection<SearchGroup<TGroupValue>> groups;
+        private readonly IEnumerable<SearchGroup<TGroupValue>> groups;
         private readonly Sort withinGroupSort;
         private readonly Sort groupSort;
 
         private int totalHitCount;
         private int totalGroupedHitCount;
 
-        public AbstractSecondPassGroupingCollector(ICollection<SearchGroup<TGroupValue>> groups, Sort groupSort, Sort withinGroupSort,
+        public AbstractSecondPassGroupingCollector(IEnumerable<SearchGroup<TGroupValue>> groups, Sort groupSort, Sort withinGroupSort,
                                                    int maxDocsPerGroup, bool getScores, bool getMaxScores, bool fillSortFields)
         {
 
             //System.out.println("SP init");
-            if (groups.Count == 0)
+            if (groups.Count() == 0)
             {
                 throw new ArgumentException("no groups to collect (groups.size() is 0)");
             }
@@ -47,7 +47,7 @@ namespace Lucene.Net.Search.Grouping
             this.withinGroupSort = withinGroupSort;
             this.groups = groups;
             this.maxDocsPerGroup = maxDocsPerGroup;
-            groupMap = new Dictionary<TGroupValue, AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue>>(groups.Count);
+            groupMap = new Dictionary<TGroupValue, AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue>>(groups.Count());
 
             foreach (SearchGroup<TGroupValue> group in groups)
             {
@@ -64,7 +64,7 @@ namespace Lucene.Net.Search.Grouping
                     // Sort by fields
                     collector = TopFieldCollector.Create(withinGroupSort, maxDocsPerGroup, fillSortFields, getScores, getMaxScores, true);
                 }
-                groupMap[group.groupValue] = new AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue>(group.groupValue, collector);
+                groupMap[group.GroupValue] = new AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue>(group.GroupValue, collector);
             }
         }
 
@@ -118,20 +118,20 @@ namespace Lucene.Net.Search.Grouping
 
         public TopGroups<TGroupValue> GetTopGroups(int withinGroupOffset)
         {
-            GroupDocs<TGroupValue>[] groupDocsResult = new GroupDocs<TGroupValue>[groups.Count];
+            GroupDocs<TGroupValue>[] groupDocsResult = new GroupDocs<TGroupValue>[groups.Count()];
 
             int groupIDX = 0;
             float maxScore = float.MinValue;
             foreach (var group in groups)
             {
-                AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue> groupDocs = groupMap.ContainsKey(group.groupValue) ? groupMap[group.groupValue] : null;
+                AbstractSecondPassGroupingCollector.SearchGroupDocs<TGroupValue> groupDocs = groupMap.ContainsKey(group.GroupValue) ? groupMap[group.GroupValue] : null;
                 TopDocs topDocs = groupDocs.collector.TopDocs(withinGroupOffset, maxDocsPerGroup);
                 groupDocsResult[groupIDX++] = new GroupDocs<TGroupValue>(float.NaN,
                                                                               topDocs.MaxScore,
                                                                               topDocs.TotalHits,
                                                                               topDocs.ScoreDocs,
                                                                               groupDocs.groupValue,
-                                                                              group.sortValues);
+                                                                              group.SortValues);
                 maxScore = Math.Max(maxScore, topDocs.MaxScore);
             }
 
@@ -167,4 +167,13 @@ namespace Lucene.Net.Search.Grouping
             }
         }
     }
+
+    /// <summary>
+    /// LUCENENET specific interface used to apply covariance to TGroupValue
+    /// </summary>
+    /// <typeparam name="TGroupValue"></typeparam>
+    public interface IAbstractSecondPassGroupingCollector<out TGroupValue>
+    {
+        ITopGroups<TGroupValue> GetTopGroups(int withinGroupOffset);
+    }
 }

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/Function/FunctionAllGroupsCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/Function/FunctionAllGroupsCollector.cs b/src/Lucene.Net.Grouping/Function/FunctionAllGroupsCollector.cs
index 638f36b..c5c7623 100644
--- a/src/Lucene.Net.Grouping/Function/FunctionAllGroupsCollector.cs
+++ b/src/Lucene.Net.Grouping/Function/FunctionAllGroupsCollector.cs
@@ -39,7 +39,7 @@ namespace Lucene.Net.Search.Grouping.Function
             this.groupBy = groupBy;
         }
 
-        public override ICollection<MutableValue> Groups
+        public override IEnumerable<MutableValue> Groups
         {
             get
             {

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/Function/FunctionDistinctValuesCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/Function/FunctionDistinctValuesCollector.cs b/src/Lucene.Net.Grouping/Function/FunctionDistinctValuesCollector.cs
index 2e84920..3d5dc0a 100644
--- a/src/Lucene.Net.Grouping/Function/FunctionDistinctValuesCollector.cs
+++ b/src/Lucene.Net.Grouping/Function/FunctionDistinctValuesCollector.cs
@@ -36,7 +36,7 @@ namespace Lucene.Net.Search.Grouping.Function
             groupMap = new LurchTable<MutableValue, GroupCount>(1 << 4);
             foreach (SearchGroup<MutableValue> group in groups)
             {
-                groupMap[group.groupValue] = new GroupCount(group.groupValue);
+                groupMap[group.GroupValue] = new GroupCount(group.GroupValue);
             }
         }
 

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/GroupDocs.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/GroupDocs.cs b/src/Lucene.Net.Grouping/GroupDocs.cs
index 29d227e..cc1a7c6 100644
--- a/src/Lucene.Net.Grouping/GroupDocs.cs
+++ b/src/Lucene.Net.Grouping/GroupDocs.cs
@@ -24,39 +24,39 @@ namespace Lucene.Net.Search.Grouping
     /// 
     /// @lucene.experimental 
     /// </summary>
-    public class GroupDocs<TGroupValue>
+    public class GroupDocs<TGroupValue> : IGroupDocs<TGroupValue>
     {
         /// <summary>
         /// The groupField value for all docs in this group; this
         /// may be null if hits did not have the groupField. 
         /// </summary>
-        public readonly TGroupValue GroupValue;
+        public TGroupValue GroupValue { get; private set; }
 
         /// <summary>
         /// Max score in this group
         /// </summary>
-        public readonly float MaxScore;
+        public float MaxScore { get; private set; }
 
         /// <summary>
         /// Overall aggregated score of this group (currently only set by join queries). 
         /// </summary>
-        public readonly float Score;
+        public float Score { get; private set; }
 
         /// <summary>
-        /// Hits; this may be {@link org.apache.lucene.search.FieldDoc} instances if the
+        /// Hits; this may be <see cref="FieldDoc"/> instances if the
         /// withinGroupSort sorted by fields. 
         /// </summary>
-        public readonly ScoreDoc[] ScoreDocs;
+        public ScoreDoc[] ScoreDocs { get; private set; }
 
         /// <summary>
         /// Total hits within this group
         /// </summary>
-        public readonly int TotalHits;
+        public int TotalHits { get; private set; }
 
         /// <summary>
-        /// Matches the groupSort passed to {@link AbstractFirstPassGroupingCollector}. 
+        /// Matches the groupSort passed to <see cref="AbstractFirstPassGroupingCollector{TGroupValue}"/>. 
         /// </summary>
-        public readonly object[] GroupSortValues;
+        public object[] GroupSortValues { get; private set; }
 
         public GroupDocs(float score, float maxScore, int totalHits, ScoreDoc[] scoreDocs, TGroupValue groupValue, object[] groupSortValues)
         {
@@ -68,4 +68,43 @@ namespace Lucene.Net.Search.Grouping
             GroupSortValues = groupSortValues;
         }
     }
+
+    /// <summary>
+    /// LUCENENET specific interface used to apply covariance to TGroupValue
+    /// </summary>
+    /// <typeparam name="TGroupValue"></typeparam>
+    public interface IGroupDocs<out TGroupValue>
+    {
+        /// <summary>
+        /// The groupField value for all docs in this group; this
+        /// may be null if hits did not have the groupField. 
+        /// </summary>
+        TGroupValue GroupValue { get; }
+
+        /// <summary>
+        /// Max score in this group
+        /// </summary>
+        float MaxScore { get; }
+
+        /// <summary>
+        /// Overall aggregated score of this group (currently only set by join queries). 
+        /// </summary>
+        float Score { get; }
+
+        /// <summary>
+        /// Hits; this may be <see cref="FieldDoc"/> instances if the
+        /// withinGroupSort sorted by fields. 
+        /// </summary>
+        ScoreDoc[] ScoreDocs { get; }
+
+        /// <summary>
+        /// Total hits within this group
+        /// </summary>
+        int TotalHits { get; }
+
+        /// <summary>
+        /// Matches the groupSort passed to <see cref="AbstractFirstPassGroupingCollector{TGroupValue}"/>. 
+        /// </summary>
+        object[] GroupSortValues { get; }
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/GroupingSearch.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/GroupingSearch.cs b/src/Lucene.Net.Grouping/GroupingSearch.cs
index 28b5381..56554f6 100644
--- a/src/Lucene.Net.Grouping/GroupingSearch.cs
+++ b/src/Lucene.Net.Grouping/GroupingSearch.cs
@@ -41,7 +41,7 @@ namespace Lucene.Net.Search.Grouping
         private bool allGroupHeads;
         private int initialSize = 128;
 
-        private ICollection /* Collection<?> */ matchingGroups;
+        private IList /* Collection<?> */ matchingGroups;
         private Bits matchingGroupHeads;
 
         /**
@@ -97,7 +97,7 @@ namespace Lucene.Net.Search.Grouping
          * @return the grouped result as a {@link TopGroups} instance
          * @throws IOException If any I/O related errors occur
          */
-        public TopGroups<TGroupValue> Search<TGroupValue>(IndexSearcher searcher, Query query, int groupOffset, int groupLimit)
+        public ITopGroups<TGroupValue> Search<TGroupValue>(IndexSearcher searcher, Query query, int groupOffset, int groupLimit)
         {
             return Search<TGroupValue>(searcher, null, query, groupOffset, groupLimit);
         }
@@ -113,7 +113,7 @@ namespace Lucene.Net.Search.Grouping
          * @return the grouped result as a {@link TopGroups} instance
          * @throws IOException If any I/O related errors occur
          */
-        public TopGroups<TGroupValue> Search<TGroupValue>(IndexSearcher searcher, Filter filter, Query query, int groupOffset, int groupLimit)
+        public ITopGroups<TGroupValue> Search<TGroupValue>(IndexSearcher searcher, Filter filter, Query query, int groupOffset, int groupLimit)
         {
             if (groupField != null || groupFunction != null)
             {
@@ -129,147 +129,151 @@ namespace Lucene.Net.Search.Grouping
             }
         }
 
-        protected TopGroups<TGroupValue> GroupByFieldOrFunction<TGroupValue>(IndexSearcher searcher, Filter filter, Query query, int groupOffset, int groupLimit)
+        protected ITopGroups<TGroupValue> GroupByFieldOrFunction<TGroupValue>(IndexSearcher searcher, Filter filter, Query query, int groupOffset, int groupLimit)
         {
-            // LUCENENET TODO: Finish
-            return null;
-            //int topN = groupOffset + groupLimit;
-            //AbstractFirstPassGroupingCollector<TGroupValue> firstPassCollector;
-            //AbstractAllGroupsCollector<TGroupValue> allGroupsCollector;
-            //AbstractAllGroupHeadsCollector allGroupHeadsCollector;
-            //if (groupFunction != null)
-            //{
-            //    firstPassCollector = new FunctionFirstPassGroupingCollector(groupFunction, valueSourceContext, groupSort, topN);
-            //    if (allGroups)
-            //    {
-            //        allGroupsCollector = new FunctionAllGroupsCollector(groupFunction, valueSourceContext);
-            //    }
-            //    else
-            //    {
-            //        allGroupsCollector = null;
-            //    }
-            //    if (allGroupHeads)
-            //    {
-            //        allGroupHeadsCollector = new FunctionAllGroupHeadsCollector(groupFunction, valueSourceContext, sortWithinGroup);
-            //    }
-            //    else
-            //    {
-            //        allGroupHeadsCollector = null;
-            //    }
-            //}
-            //else
-            //{
-            //    firstPassCollector = new TermFirstPassGroupingCollector(groupField, groupSort, topN);
-            //    if (allGroups)
-            //    {
-            //        allGroupsCollector = new TermAllGroupsCollector(groupField, initialSize);
-            //    }
-            //    else
-            //    {
-            //        allGroupsCollector = null;
-            //    }
-            //    if (allGroupHeads)
-            //    {
-            //        allGroupHeadsCollector = TermAllGroupHeadsCollector.Create(groupField, sortWithinGroup, initialSize);
-            //    }
-            //    else
-            //    {
-            //        allGroupHeadsCollector = null;
-            //    }
-            //}
-
-            //Collector firstRound;
-            //if (allGroupHeads || allGroups)
-            //{
-            //    List<Collector> collectors = new List<Collector>();
-            //    collectors.Add(firstPassCollector);
-            //    if (allGroups)
-            //    {
-            //        collectors.Add(allGroupsCollector);
-            //    }
-            //    if (allGroupHeads)
-            //    {
-            //        collectors.Add(allGroupHeadsCollector);
-            //    }
-            //    firstRound = MultiCollector.Wrap(collectors.ToArray(/* new Collector[collectors.size()] */));
-            //}
-            //else
-            //{
-            //    firstRound = firstPassCollector;
-            //}
-
-            //CachingCollector cachedCollector = null;
-            //if (maxCacheRAMMB != null || maxDocsToCache != null)
-            //{
-            //    if (maxCacheRAMMB != null)
-            //    {
-            //        cachedCollector = CachingCollector.Create(firstRound, cacheScores, maxCacheRAMMB.Value);
-            //    }
-            //    else
-            //    {
-            //        cachedCollector = CachingCollector.Create(firstRound, cacheScores, maxDocsToCache.Value);
-            //    }
-            //    searcher.Search(query, filter, cachedCollector);
-            //}
-            //else
-            //{
-            //    searcher.Search(query, filter, firstRound);
-            //}
-
-            //if (allGroups)
-            //{
-            //    matchingGroups = (ICollection)allGroupsCollector.GetGroups();
-            //}
-            //else
-            //{
-            //    matchingGroups = (ICollection)Collections.EmptyList<TGroupValue>();
-            //}
-            //if (allGroupHeads)
-            //{
-            //    matchingGroupHeads = allGroupHeadsCollector.RetrieveGroupHeads(searcher.IndexReader.MaxDoc);
-            //}
-            //else
-            //{
-            //    matchingGroupHeads = new Bits_MatchNoBits(searcher.IndexReader.MaxDoc);
-            //}
-
-            //ICollection<SearchGroup<TGroupValue>> topSearchGroups = firstPassCollector.GetTopGroups(groupOffset, fillSortFields);
-            //if (topSearchGroups == null)
-            //{
-            //    return new TopGroups<TGroupValue>(new SortField[0], new SortField[0], 0, 0, new GroupDocs<TGroupValue>[0], float.NaN);
-            //}
-
-            //int topNInsideGroup = groupDocsOffset + groupDocsLimit;
-            //AbstractSecondPassGroupingCollector<TGroupValue> secondPassCollector;
-            //if (groupFunction != null)
-            //{
-            //    secondPassCollector = new FunctionSecondPassGroupingCollector(topSearchGroups as ICollection<SearchGroup<MutableValue>>, groupSort, sortWithinGroup, topNInsideGroup, includeScores, includeMaxScore, fillSortFields, groupFunction, valueSourceContext);
-            //}
-            //else
-            //{
-            //    secondPassCollector = new TermSecondPassGroupingCollector(groupField, topSearchGroups as ICollection<SearchGroup<BytesRef>>, groupSort, sortWithinGroup, topNInsideGroup, includeScores, includeMaxScore, fillSortFields);
-            //}
-
-            //if (cachedCollector != null && cachedCollector.Cached)
-            //{
-            //    cachedCollector.Replay(secondPassCollector);
-            //}
-            //else
-            //{
-            //    searcher.Search(query, filter, secondPassCollector);
-            //}
-
-            //if (allGroups)
-            //{
-            //    return new TopGroups<TGroupValue>(secondPassCollector.GetTopGroups(groupDocsOffset), matchingGroups.Count);
-            //}
-            //else
-            //{
-            //    return secondPassCollector.GetTopGroups(groupDocsOffset);
-            //}
+            int topN = groupOffset + groupLimit;
+            IAbstractFirstPassGroupingCollector<TGroupValue> firstPassCollector;
+            IAbstractAllGroupsCollector<TGroupValue> allGroupsCollector;
+            AbstractAllGroupHeadsCollector allGroupHeadsCollector;
+            if (groupFunction != null)
+            {
+                firstPassCollector = (IAbstractFirstPassGroupingCollector<TGroupValue>)new FunctionFirstPassGroupingCollector(groupFunction, valueSourceContext, groupSort, topN);
+                if (allGroups)
+                {
+                    allGroupsCollector = (IAbstractAllGroupsCollector<TGroupValue>)new FunctionAllGroupsCollector(groupFunction, valueSourceContext);
+                }
+                else
+                {
+                    allGroupsCollector = null;
+                }
+                if (allGroupHeads)
+                {
+                    allGroupHeadsCollector = new FunctionAllGroupHeadsCollector(groupFunction, valueSourceContext, sortWithinGroup);
+                }
+                else
+                {
+                    allGroupHeadsCollector = null;
+                }
+            }
+            else
+            {
+                firstPassCollector = (IAbstractFirstPassGroupingCollector<TGroupValue>)new TermFirstPassGroupingCollector(groupField, groupSort, topN);
+                if (allGroups)
+                {
+                    allGroupsCollector = (IAbstractAllGroupsCollector<TGroupValue>)new TermAllGroupsCollector(groupField, initialSize);
+                }
+                else
+                {
+                    allGroupsCollector = null;
+                }
+                if (allGroupHeads)
+                {
+                    allGroupHeadsCollector = TermAllGroupHeadsCollector.Create(groupField, sortWithinGroup, initialSize);
+                }
+                else
+                {
+                    allGroupHeadsCollector = null;
+                }
+            }
+
+            Collector firstRound;
+            if (allGroupHeads || allGroups)
+            {
+                List<Collector> collectors = new List<Collector>();
+                // LUCENENET TODO: Make the Collector abstract class into an interface
+                // so we can remove the casting here
+                collectors.Add((Collector)firstPassCollector);
+                if (allGroups)
+                {
+                    // LUCENENET TODO: Make the Collector abstract class into an interface
+                    // so we can remove the casting here
+                    collectors.Add((Collector)allGroupsCollector);
+                }
+                if (allGroupHeads)
+                {
+                    collectors.Add(allGroupHeadsCollector);
+                }
+                firstRound = MultiCollector.Wrap(collectors.ToArray(/* new Collector[collectors.size()] */));
+            }
+            else
+            {
+                // LUCENENET TODO: Make the Collector abstract class into an interface
+                // so we can remove the casting here
+                firstRound = (Collector)firstPassCollector;
+            }
+
+            CachingCollector cachedCollector = null;
+            if (maxCacheRAMMB != null || maxDocsToCache != null)
+            {
+                if (maxCacheRAMMB != null)
+                {
+                    cachedCollector = CachingCollector.Create(firstRound, cacheScores, maxCacheRAMMB.Value);
+                }
+                else
+                {
+                    cachedCollector = CachingCollector.Create(firstRound, cacheScores, maxDocsToCache.Value);
+                }
+                searcher.Search(query, filter, cachedCollector);
+            }
+            else
+            {
+                searcher.Search(query, filter, firstRound);
+            }
+
+            if (allGroups)
+            {
+                matchingGroups = (IList)allGroupsCollector.Groups;
+            }
+            else
+            {
+                matchingGroups = new List<TGroupValue>();
+            }
+            if (allGroupHeads)
+            {
+                matchingGroupHeads = allGroupHeadsCollector.RetrieveGroupHeads(searcher.IndexReader.MaxDoc);
+            }
+            else
+            {
+                matchingGroupHeads = new Bits_MatchNoBits(searcher.IndexReader.MaxDoc);
+            }
+
+            IEnumerable<ISearchGroup<TGroupValue>> topSearchGroups = firstPassCollector.GetTopGroups(groupOffset, fillSortFields);
+            if (topSearchGroups == null)
+            {
+                return new TopGroups<TGroupValue>(new SortField[0], new SortField[0], 0, 0, new GroupDocs<TGroupValue>[0], float.NaN);
+            }
+
+            int topNInsideGroup = groupDocsOffset + groupDocsLimit;
+            IAbstractSecondPassGroupingCollector<TGroupValue> secondPassCollector;
+            if (groupFunction != null)
+            {
+                secondPassCollector = (IAbstractSecondPassGroupingCollector<TGroupValue>)new FunctionSecondPassGroupingCollector(topSearchGroups as ICollection<SearchGroup<MutableValue>>, groupSort, sortWithinGroup, topNInsideGroup, includeScores, includeMaxScore, fillSortFields, groupFunction, valueSourceContext);
+            }
+            else
+            {
+                secondPassCollector = (IAbstractSecondPassGroupingCollector<TGroupValue>)new TermSecondPassGroupingCollector(groupField, topSearchGroups as ICollection<SearchGroup<BytesRef>>, groupSort, sortWithinGroup, topNInsideGroup, includeScores, includeMaxScore, fillSortFields);
+            }
+
+            if (cachedCollector != null && cachedCollector.Cached)
+            {
+                cachedCollector.Replay((Collector)secondPassCollector);
+            }
+            else
+            {
+                searcher.Search(query, filter, (Collector)secondPassCollector);
+            }
+
+            if (allGroups)
+            {
+                return new TopGroups<TGroupValue>(secondPassCollector.GetTopGroups(groupDocsOffset), matchingGroups.Count);
+            }
+            else
+            {
+                return secondPassCollector.GetTopGroups(groupDocsOffset);
+            }
         }
 
-        protected TopGroups<T> GroupByDocBlock<T>(IndexSearcher searcher, Filter filter, Query query, int groupOffset, int groupLimit)
+        protected ITopGroups<T> GroupByDocBlock<T>(IndexSearcher searcher, Filter filter, Query query, int groupOffset, int groupLimit)
         {
             int topN = groupOffset + groupLimit;
             BlockGroupingCollector c = new BlockGroupingCollector(groupSort, topN, includeScores, groupEndDocs);

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/SearchGroup.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/SearchGroup.cs b/src/Lucene.Net.Grouping/SearchGroup.cs
index 6ab902f..693e513 100644
--- a/src/Lucene.Net.Grouping/SearchGroup.cs
+++ b/src/Lucene.Net.Grouping/SearchGroup.cs
@@ -14,21 +14,21 @@ namespace Lucene.Net.Search.Grouping
     /// @lucene.experimental
     /// </summary>
     /// <typeparam name="TGroupValue"></typeparam>
-    public class SearchGroup<TGroupValue>
+    public class SearchGroup<TGroupValue> : ISearchGroup<TGroupValue>
     {
         /** The value that defines this group  */
-        public TGroupValue groupValue;
+        public TGroupValue GroupValue { get; set; }
 
         /** The sort values used during sorting. These are the
          *  groupSort field values of the highest rank document
          *  (by the groupSort) within the group.  Can be
          * <code>null</code> if <code>fillFields=false</code> had
          * been passed to {@link AbstractFirstPassGroupingCollector#getTopGroups} */
-        public object[] sortValues;
+        public object[] SortValues { get; set; }
 
         public override string ToString()
         {
-            return ("SearchGroup(groupValue=" + groupValue + " sortValues=" + Arrays.ToString(sortValues) + ")");
+            return ("SearchGroup(groupValue=" + GroupValue + " sortValues=" + Arrays.ToString(SortValues) + ")");
         }
 
         public override bool Equals(object o)
@@ -38,14 +38,14 @@ namespace Lucene.Net.Search.Grouping
 
             SearchGroup<TGroupValue> that = (SearchGroup<TGroupValue>)o;
 
-            if (groupValue == null)
+            if (GroupValue == null)
             {
-                if (that.groupValue != null)
+                if (that.GroupValue != null)
                 {
                     return false;
                 }
             }
-            else if (!groupValue.Equals(that.groupValue))
+            else if (!GroupValue.Equals(that.GroupValue))
             {
                 return false;
             }
@@ -55,7 +55,7 @@ namespace Lucene.Net.Search.Grouping
 
         public override int GetHashCode()
         {
-            return groupValue != null ? groupValue.GetHashCode() : 0;
+            return GroupValue != null ? GroupValue.GetHashCode() : 0;
         }
 
         private class ShardIter<T>
@@ -74,7 +74,7 @@ namespace Lucene.Net.Search.Grouping
             {
                 Debug.Assert(iter.MoveNext());
                 SearchGroup<T> group = iter.Current;
-                if (group.sortValues == null)
+                if (group.SortValues == null)
                 {
                     throw new ArgumentException("group.sortValues is null; you must pass fillFields=true to the first pass collector");
                 }
@@ -224,7 +224,7 @@ namespace Lucene.Net.Search.Grouping
                 while (shard.iter.MoveNext())
                 {
                     SearchGroup<T> group = shard.Next();
-                    MergedGroup<T> mergedGroup = groupsSeen.ContainsKey(group.groupValue) ? groupsSeen[group.groupValue] : null;
+                    MergedGroup<T> mergedGroup = groupsSeen.ContainsKey(group.GroupValue) ? groupsSeen[group.GroupValue] : null;
                     bool isNew = mergedGroup == null;
                     //System.out.println("    next group=" + (group.groupValue == null ? "null" : ((BytesRef) group.groupValue).utf8ToString()) + " sort=" + Arrays.toString(group.sortValues));
 
@@ -232,11 +232,11 @@ namespace Lucene.Net.Search.Grouping
                     {
                         // Start a new group:
                         //System.out.println("      new");
-                        mergedGroup = new MergedGroup<T>(group.groupValue);
+                        mergedGroup = new MergedGroup<T>(group.GroupValue);
                         mergedGroup.minShardIndex = shard.shardIndex;
-                        Debug.Assert(group.sortValues != null);
-                        mergedGroup.topValues = group.sortValues;
-                        groupsSeen[group.groupValue] = mergedGroup;
+                        Debug.Assert(group.SortValues != null);
+                        mergedGroup.topValues = group.SortValues;
+                        groupsSeen[group.GroupValue] = mergedGroup;
                         mergedGroup.inQueue = true;
                         queue.Add(mergedGroup);
                     }
@@ -252,7 +252,7 @@ namespace Lucene.Net.Search.Grouping
                         bool competes = false;
                         for (int compIDX = 0; compIDX < groupComp.comparators.Length; compIDX++)
                         {
-                            int cmp = groupComp.reversed[compIDX] * groupComp.comparators[compIDX].CompareValues(group.sortValues[compIDX],
+                            int cmp = groupComp.reversed[compIDX] * groupComp.comparators[compIDX].CompareValues(group.SortValues[compIDX],
                                                                                                                        mergedGroup.topValues[compIDX]);
                             if (cmp < 0)
                             {
@@ -283,7 +283,7 @@ namespace Lucene.Net.Search.Grouping
                             {
                                 queue.Remove(mergedGroup);
                             }
-                            mergedGroup.topValues = group.sortValues;
+                            mergedGroup.topValues = group.SortValues;
                             mergedGroup.minShardIndex = shard.shardIndex;
                             queue.Add(mergedGroup);
                             mergedGroup.inQueue = true;
@@ -335,8 +335,8 @@ namespace Lucene.Net.Search.Grouping
                     if (count++ >= offset)
                     {
                         SearchGroup<T> newGroup = new SearchGroup<T>();
-                        newGroup.groupValue = group.groupValue;
-                        newGroup.sortValues = group.topValues;
+                        newGroup.GroupValue = group.groupValue;
+                        newGroup.SortValues = group.topValues;
                         newTopGroups.Add(newGroup);
                         if (newTopGroups.Count == topN)
                         {
@@ -385,4 +385,15 @@ namespace Lucene.Net.Search.Grouping
             }
         }
     }
+
+    /// <summary>
+    /// LUCENENET specific interface used to provide covariance
+    /// with the TGroupValue type
+    /// </summary>
+    /// <typeparam name="TGroupValue"></typeparam>
+    public interface ISearchGroup<out TGroupValue>
+    {
+        TGroupValue GroupValue { get; }
+        object[] SortValues { get; set; }
+    }
 }

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/Term/TermAllGroupsCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/Term/TermAllGroupsCollector.cs b/src/Lucene.Net.Grouping/Term/TermAllGroupsCollector.cs
index 7693d93..ef33aa5 100644
--- a/src/Lucene.Net.Grouping/Term/TermAllGroupsCollector.cs
+++ b/src/Lucene.Net.Grouping/Term/TermAllGroupsCollector.cs
@@ -83,7 +83,7 @@ namespace Lucene.Net.Search.Grouping.Terms
             }
         }
 
-        public override ICollection<BytesRef> Groups
+        public override IEnumerable<BytesRef> Groups
         {
             get
             {

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/Term/TermDistinctValuesCollector.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/Term/TermDistinctValuesCollector.cs b/src/Lucene.Net.Grouping/Term/TermDistinctValuesCollector.cs
index d6f6bab..502b0ea 100644
--- a/src/Lucene.Net.Grouping/Term/TermDistinctValuesCollector.cs
+++ b/src/Lucene.Net.Grouping/Term/TermDistinctValuesCollector.cs
@@ -41,7 +41,7 @@ namespace Lucene.Net.Search.Grouping.Terms
             this.groups = new List<GroupCount>(groups.Count);
             foreach (SearchGroup<BytesRef> group in groups)
             {
-                this.groups.Add(new GroupCount(group.groupValue));
+                this.groups.Add(new GroupCount(group.GroupValue));
             }
             ordSet = new SentinelIntSet(groups.Count, -2);
             groupCounts = new GroupCount[ordSet.Keys.Length];

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/0a137bea/src/Lucene.Net.Grouping/TopGroups.cs
----------------------------------------------------------------------
diff --git a/src/Lucene.Net.Grouping/TopGroups.cs b/src/Lucene.Net.Grouping/TopGroups.cs
index 091103d..b5d5c65 100644
--- a/src/Lucene.Net.Grouping/TopGroups.cs
+++ b/src/Lucene.Net.Grouping/TopGroups.cs
@@ -24,39 +24,39 @@ namespace Lucene.Net.Search.Grouping
     /// 
     /// @lucene.experimental 
     /// </summary>
-    public class TopGroups<TGroupValue>
+    public class TopGroups<TGroupValue> : ITopGroups<TGroupValue>
     {
         /// <summary>
         /// Number of documents matching the search </summary>
-        public readonly int TotalHitCount;
+        public int TotalHitCount { get; private set; }
 
         /// <summary>
         /// Number of documents grouped into the topN groups </summary>
-        public readonly int TotalGroupedHitCount;
+        public int TotalGroupedHitCount { get; private set; }
 
         /// <summary>
-        /// The total number of unique groups. If <code>null</code> this value is not computed. </summary>
-        public readonly int? TotalGroupCount;
+        /// The total number of unique groups. If <c>null</c> this value is not computed. </summary>
+        public int? TotalGroupCount { get; private set; }
 
         /// <summary>
         /// Group results in groupSort order </summary>
-        public readonly GroupDocs<TGroupValue>[] Groups;
+        public IGroupDocs<TGroupValue>[] Groups { get; private set; }
 
         /// <summary>
         /// How groups are sorted against each other </summary>
-        public readonly SortField[] GroupSort;
+        public SortField[] GroupSort { get; private set; }
 
         /// <summary>
         /// How docs are sorted within each group </summary>
-        public readonly SortField[] WithinGroupSort;
+        public SortField[] WithinGroupSort { get; private set; }
 
         /// <summary>
         /// Highest score across all hits, or
-        ///  <code>Float.NaN</code> if scores were not computed. 
+        /// <see cref="float.NaN"/> if scores were not computed. 
         /// </summary>
-        public readonly float MaxScore;
+        public float MaxScore { get; private set; }
 
-        public TopGroups(SortField[] groupSort, SortField[] withinGroupSort, int totalHitCount, int totalGroupedHitCount, GroupDocs<TGroupValue>[] groups, float maxScore)
+        public TopGroups(SortField[] groupSort, SortField[] withinGroupSort, int totalHitCount, int totalGroupedHitCount, IGroupDocs<TGroupValue>[] groups, float maxScore)
         {
             GroupSort = groupSort;
             WithinGroupSort = withinGroupSort;
@@ -67,7 +67,7 @@ namespace Lucene.Net.Search.Grouping
             MaxScore = maxScore;
         }
 
-        public TopGroups(TopGroups<TGroupValue> oldTopGroups, int? totalGroupCount)
+        public TopGroups(ITopGroups<TGroupValue> oldTopGroups, int? totalGroupCount)
         {
             GroupSort = oldTopGroups.GroupSort;
             WithinGroupSort = oldTopGroups.WithinGroupSort;
@@ -245,4 +245,42 @@ namespace Lucene.Net.Search.Grouping
             return new TopGroups<T>(groupSort.GetSort(), docSort == null ? null : docSort.GetSort(), totalHitCount, totalGroupedHitCount, mergedGroupDocs, totalMaxScore);
         }
     }
+
+    /// <summary>
+    /// LUCENENET specific interface used to provide covariance
+    /// with the TGroupValue type
+    /// </summary>
+    /// <typeparam name="TGroupValue"></typeparam>
+    public interface ITopGroups<out TGroupValue>
+    {
+        /// <summary>
+        /// Number of documents matching the search </summary>
+        int TotalHitCount { get; }
+
+        /// <summary>
+        /// Number of documents grouped into the topN groups </summary>
+        int TotalGroupedHitCount { get; }
+
+        /// <summary>
+        /// The total number of unique groups. If <c>null</c> this value is not computed. </summary>
+        int? TotalGroupCount { get; }
+
+        /// <summary>
+        /// Group results in groupSort order </summary>
+        IGroupDocs<TGroupValue>[] Groups { get; }
+
+        /// <summary>
+        /// How groups are sorted against each other </summary>
+        SortField[] GroupSort { get; }
+
+        /// <summary>
+        /// How docs are sorted within each group </summary>
+        SortField[] WithinGroupSort { get; }
+
+        /// <summary>
+        /// Highest score across all hits, or
+        /// <see cref="float.NaN"/> if scores were not computed. 
+        /// </summary>
+        float MaxScore { get; }
+    }
 }
\ No newline at end of file