Index: lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java
===================================================================
--- lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java	(revision 1343259)
+++ lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java	(working copy)
@@ -115,6 +115,7 @@
 
     final GroupDocs<Integer> group = results.groups[0];
     assertEquals(1, group.totalHits);
+    assertFalse(Float.isNaN(group.score));
 
     Document childDoc = s.doc(group.scoreDocs[0].doc);
     //System.out.println("  doc=" + group.scoreDocs[0].doc);
Index: lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinCollector.java
===================================================================
--- lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinCollector.java	(revision 1343259)
+++ lucene/join/src/java/org/apache/lucene/search/join/ToParentBlockJoinCollector.java	(working copy)
@@ -146,9 +146,7 @@
 
     if (trackMaxScore) {
       score = scorer.score();
-      if (score > maxScore) {
-        maxScore = score;
-      }
+      maxScore = Math.max(score, maxScore);
     }
 
     // TODO: we could sweep all joinScorers here and
@@ -202,7 +200,11 @@
       for (int i = 0; i < comparators.length; i++) {
         comparators[i].copy(comparatorSlot, parentDoc);
       }
-      //System.out.println("  startup: new OG doc=" + (docBase+parentDoc));
+      //System.out.println("  startup: new OG doc=" +
+      //(docBase+parentDoc));
+      if (!trackMaxScore && trackScores) {
+        score = scorer.score();
+      }
       final OneGroup og = new OneGroup(comparatorSlot, docBase+parentDoc, score, joinScorers.length, trackScores);
       og.readerContext = currentReaderContext;
       copyGroups(og);
@@ -431,7 +433,8 @@
 
       final TopDocs topDocs = collector.topDocs(withinGroupOffset, maxDocsPerGroup);
 
-      groups[groupIDX-offset] = new GroupDocs<Integer>(topDocs.getMaxScore(),
+      groups[groupIDX-offset] = new GroupDocs<Integer>(og.score,
+                                                       topDocs.getMaxScore(),
                                                        og.counts[slot],
                                                        topDocs.scoreDocs,
                                                        og.doc,
@@ -443,4 +446,12 @@
                                                          0, totalGroupedHitCount, groups),
                                   totalHitCount);
   }
+
+  /** Returns the highest score across all collected parent
+   *  hits, as long as <code>trackMaxScores=true</code> was passed {@link
+   *  #ToParentBlockJoinCollector on construction}.  Else,
+   *  this returns <code>Float.NaN</code> */
+  public float getMaxScore() {
+    return maxScore;
+  }
 }
Index: lucene/CHANGES.txt
===================================================================
--- lucene/CHANGES.txt	(revision 1343259)
+++ lucene/CHANGES.txt	(working copy)
@@ -979,6 +979,9 @@
   addTaxonomy and now takes only one Directory and one OrdinalMap.
   (Shai Erera, Gilad Barkai)
 
+* LUCENE-4077: Expose the max score and per-group scores from
+  ToParentBlockJoinCollector (Christoph Kaser, Mike McCandless)
+
 Documentation
 
 * LUCENE-3958: Javadocs corrections for IndexWriter.
Index: lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java
===================================================================
--- lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java	(revision 1343259)
+++ lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java	(working copy)
@@ -341,7 +341,7 @@
       List<GroupDocs<BytesRef>> groups = new ArrayList<GroupDocs<BytesRef>>(mvalTopGroups.groups.length);
       for (GroupDocs<MutableValue> mvalGd : mvalTopGroups.groups) {
         BytesRef groupValue = mvalGd.groupValue.exists() ? ((MutableValueStr) mvalGd.groupValue).value : null;
-        groups.add(new GroupDocs<BytesRef>(mvalGd.maxScore, mvalGd.totalHits, mvalGd.scoreDocs, groupValue, mvalGd.groupSortValues));
+        groups.add(new GroupDocs<BytesRef>(Float.NaN, mvalGd.maxScore, mvalGd.totalHits, mvalGd.scoreDocs, groupValue, mvalGd.groupSortValues));
       }
       return new TopGroups<BytesRef>(mvalTopGroups.groupSort, mvalTopGroups.withinGroupSort, mvalTopGroups.totalHitCount, mvalTopGroups.totalGroupedHitCount, groups.toArray(new GroupDocs[groups.size()]));
     } else if (DVSecondPassGroupingCollector.class.isAssignableFrom(c.getClass())) {
@@ -541,11 +541,12 @@
         hits = new ScoreDoc[0];
       }
 
-      result[idx-groupOffset] = new GroupDocs<BytesRef>(0.0f,
-                                              docs.size(),
-                                              hits,
-                                              group,
-                                              fillFields ? sortedGroupFields.get(idx) : null);
+      result[idx-groupOffset] = new GroupDocs<BytesRef>(Float.NaN,
+                                                        0.0f,
+                                                        docs.size(),
+                                                        hits,
+                                                        group,
+                                                        fillFields ? sortedGroupFields.get(idx) : null);
     }
 
     if (doAllGroups) {
@@ -1238,7 +1239,7 @@
         }
       }
 
-      TopGroups<BytesRef> mergedGroups = TopGroups.merge(shardTopGroups, groupSort, docSort, docOffset, topNDocs);
+      TopGroups<BytesRef> mergedGroups = TopGroups.merge(shardTopGroups, groupSort, docSort, docOffset, topNDocs, TopGroups.ScoreMergeMode.None);
       if (VERBOSE) {
         System.out.println(" " + mergedGroups.groups.length + " merged groups:");
         for(GroupDocs<BytesRef> group : mergedGroups.groups) {
Index: lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java
===================================================================
--- lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java	(revision 1343259)
+++ lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java	(working copy)
@@ -64,6 +64,16 @@
     this.totalGroupCount = totalGroupCount;
   }
 
+  /** How the GroupDocs score (if any) should be merged. */
+  public enum ScoreMergeMode {
+    /** Set score to Float.NaN */
+    None,     
+    /* Sum score across all shards for this group. */
+    Total,
+    /* Avg score across all shards for this group. */
+    Avg,
+  };
+
   /** Merges an array of TopGroups, for example obtained
    *  from the second-pass collector across multiple
    *  shards.  Each TopGroups must have been sorted by the
@@ -81,7 +91,7 @@
    * <b>NOTE</b>: the topDocs in each GroupDocs is actually
    * an instance of TopDocsAndShards
    */
-  public static <T> TopGroups<T> merge(TopGroups<T>[] shardGroups, Sort groupSort, Sort docSort, int docOffset, int docTopN)
+  public static <T> TopGroups<T> merge(TopGroups<T>[] shardGroups, Sort groupSort, Sort docSort, int docOffset, int docTopN, ScoreMergeMode scoreMergeMode)
     throws IOException {
 
     //System.out.println("TopGroups.merge");
@@ -121,6 +131,7 @@
       //System.out.println("  merge groupValue=" + groupValue + " sortValues=" + Arrays.toString(shardGroups[0].groups[groupIDX].groupSortValues));
       float maxScore = Float.MIN_VALUE;
       int totalHits = 0;
+      double scoreSum = 0.0;
       for(int shardIDX=0;shardIDX<shardGroups.length;shardIDX++) {
         //System.out.println("    shard=" + shardIDX);
         final TopGroups<T> shard = shardGroups[shardIDX];
@@ -144,6 +155,7 @@
                                              shardGroupDocs.maxScore);
         maxScore = Math.max(maxScore, shardGroupDocs.maxScore);
         totalHits += shardGroupDocs.totalHits;
+        scoreSum += shardGroupDocs.score;
       }
 
       final TopDocs mergedTopDocs = TopDocs.merge(docSort, docOffset + docTopN, shardTopDocs);
@@ -162,8 +174,29 @@
                          0,
                          mergedTopDocs.scoreDocs.length - docOffset);
       }
+
+      final float groupScore;
+      switch(scoreMergeMode) {
+      case None:
+        groupScore = Float.NaN;
+        break;
+      case Avg:
+        if (totalHits > 0) {
+          groupScore = (float) (scoreSum / totalHits);
+        } else {
+          groupScore = Float.NaN;
+        }
+        break;
+      case Total:
+        groupScore = (float) scoreSum;
+        break;
+      default:
+        throw new IllegalArgumentException("can't handle ScoreMergeMode " + scoreMergeMode);
+      }
+        
       //System.out.println("SHARDS=" + Arrays.toString(mergedTopDocs.shardIndex));
-      mergedGroupDocs[groupIDX] = new GroupDocs<T>(maxScore,
+      mergedGroupDocs[groupIDX] = new GroupDocs<T>(groupScore,
+                                                   maxScore,
                                                    totalHits,
                                                    mergedScoreDocs,
                                                    groupValue,
Index: lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupDocs.java
===================================================================
--- lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupDocs.java	(revision 1343259)
+++ lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupDocs.java	(working copy)
@@ -30,6 +30,10 @@
   /** Max score in this group */
   public final float maxScore;
 
+  /** Overall aggregated score of this group (currently only
+   *  set by join queries). */
+  public final float score;
+
   /** Hits; this may be {@link
    * org.apache.lucene.search.FieldDoc} instances if the
    * withinGroupSort sorted by fields. */
@@ -42,11 +46,13 @@
    *  AbstractFirstPassGroupingCollector}. */
   public final Object[] groupSortValues;
 
-  public GroupDocs(float maxScore,
+  public GroupDocs(float score,
+                   float maxScore,
                    int totalHits,
                    ScoreDoc[] scoreDocs,
                    GROUP_VALUE_TYPE groupValue,
                    Object[] groupSortValues) {
+    this.score = score;
     this.maxScore = maxScore;
     this.totalHits = totalHits;
     this.scoreDocs = scoreDocs;
Index: lucene/grouping/src/java/org/apache/lucene/search/grouping/AbstractSecondPassGroupingCollector.java
===================================================================
--- lucene/grouping/src/java/org/apache/lucene/search/grouping/AbstractSecondPassGroupingCollector.java	(revision 1343259)
+++ lucene/grouping/src/java/org/apache/lucene/search/grouping/AbstractSecondPassGroupingCollector.java	(working copy)
@@ -127,7 +127,8 @@
     for(SearchGroup<?> group : groups) {
       final SearchGroupDocs<GROUP_VALUE_TYPE> groupDocs = groupMap.get(group.groupValue);
       final TopDocs topDocs = groupDocs.collector.topDocs(withinGroupOffset, maxDocsPerGroup);
-      groupDocsResult[groupIDX++] = new GroupDocs<GROUP_VALUE_TYPE>(topDocs.getMaxScore(),
+      groupDocsResult[groupIDX++] = new GroupDocs<GROUP_VALUE_TYPE>(Float.NaN,
+                                                                    topDocs.getMaxScore(),
                                                                     topDocs.totalHits,
                                                                     topDocs.scoreDocs,
                                                                     groupDocs.groupValue,
Index: lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollector.java
===================================================================
--- lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollector.java	(revision 1343259)
+++ lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollector.java	(working copy)
@@ -351,11 +351,14 @@
 
       final TopDocs topDocs = collector.topDocs(withinGroupOffset, maxDocsPerGroup);
 
-      groups[downTo] = new GroupDocs<Object>(topDocs.getMaxScore(),
-                                     og.count,
-                                     topDocs.scoreDocs,
-                                     null,
-                                     groupSortValues);
+      // TODO: we could aggregate scores across children
+      // by Sum/Avg instead of passing NaN:
+      groups[downTo] = new GroupDocs<Object>(Float.NaN,
+                                             topDocs.getMaxScore(),
+                                             og.count,
+                                             topDocs.scoreDocs,
+                                             null,
+                                             groupSortValues);
     }
 
     /*
Index: solr/core/src/java/org/apache/solr/search/Grouping.java
===================================================================
--- solr/core/src/java/org/apache/solr/search/Grouping.java	(revision 1343259)
+++ solr/core/src/java/org/apache/solr/search/Grouping.java	(working copy)
@@ -858,7 +858,7 @@
     protected void finish() throws IOException {
       TopDocsCollector topDocsCollector = (TopDocsCollector) collector.getDelegate();
       TopDocs topDocs = topDocsCollector.topDocs();
-      GroupDocs<String> groupDocs = new GroupDocs<String>(topDocs.getMaxScore(), topDocs.totalHits, topDocs.scoreDocs, query.toString(), null);
+      GroupDocs<String> groupDocs = new GroupDocs<String>(Float.NaN, topDocs.getMaxScore(), topDocs.totalHits, topDocs.scoreDocs, query.toString(), null);
       if (main) {
         mainResult = getDocList(groupDocs);
       } else {
Index: solr/core/src/java/org/apache/solr/search/grouping/distributed/shardresultserializer/TopGroupsResultTransformer.java
===================================================================
--- solr/core/src/java/org/apache/solr/search/grouping/distributed/shardresultserializer/TopGroupsResultTransformer.java	(revision 1343259)
+++ solr/core/src/java/org/apache/solr/search/grouping/distributed/shardresultserializer/TopGroupsResultTransformer.java	(working copy)
@@ -142,7 +142,7 @@
         }
 
         BytesRef groupValueRef = groupValue != null ? new BytesRef(groupValue) : null;
-        groupDocs.add(new GroupDocs<BytesRef>(maxScore, totalGroupHits, scoreDocs, groupValueRef, null));
+        groupDocs.add(new GroupDocs<BytesRef>(Float.NaN, maxScore, totalGroupHits, scoreDocs, groupValueRef, null));
       }
 
       @SuppressWarnings("unchecked")
Index: solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
===================================================================
--- solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java	(revision 1343259)
+++ solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java	(working copy)
@@ -97,7 +97,7 @@
         }
 
         TopGroups<BytesRef>[] topGroupsArr = new TopGroups[topGroups.size()];
-        rb.mergedTopGroups.put(groupField, TopGroups.merge(topGroups.toArray(topGroupsArr), groupSort, sortWithinGroup, groupOffsetDefault, docsPerGroupDefault));
+        rb.mergedTopGroups.put(groupField, TopGroups.merge(topGroups.toArray(topGroupsArr), groupSort, sortWithinGroup, groupOffsetDefault, docsPerGroupDefault, TopGroups.ScoreMergeMode.None));
       }
 
       for (String query : commandTopDocs.keySet()) {
