Repository: searchbox-io/Jest (java)

Issue:
Add a TopHits aggregation
ES provides a Top Hits sub aggregation : https://www.elastic.co/guide/en/elasticsearch/reference/1.7/search-aggregations-metrics-top-hits-aggregation.html 
https://github.com/elastic/elasticsearch/blob/master/docs/java-api/aggregations/metrics/tophits-aggregation.asciidoc

Using jest, we can query ES by using the AggregationBuilders.topHits() query builder, but there is no way to get the result since the TopHits aggregation type does not exist in Jest.
We can get the parent aggregation : 
``` 
result.getAggregations().getTermsAggregation("label.raw").getBuckets()
```
But once we have the bucket entry, we can not get the TopHits subaggregation.

Pull request description:
impl TopHitsAggregation
closes #291 

Hello,

i use a simple solution and use the SearchResult.class for reading the source from the topHits. I hope it's useful and a good way to solve this issue.

Thanks
Marcel

```
TermsBuilder testAgg = AggregationBuilders.terms("test").field("testField").size(10000);
TopHitsBuilder topHitAgg = AggregationBuilders.topHits("top_hits").setSize(100).setFetchSource(true);
configAgg.subAggregation(topHitAgg);

searchQuery.addAggregations(configAgg);

try {
   result = client.execute(searchQuery.build());
} catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

MetricAggregation aggs = result.getAggregations();
	
TermsAggregation configAggs = aggs.getTermsAggregation("test");

if (null != configAggs) {
  for (Entry bucket : configAggs.getBuckets()) {
    String configSku = bucket.getKey();
		
    TopHitsAggregation topHitResults = bucket.getTopHitsAggregation("top_hits");	
		
    List<Hit< class, Void>> articles = topHitResults.getHits(<class>.class);

    ....
  }
}
```

Patch:
diff --git a/jest-common/src/main/java/io/searchbox/core/search/aggregation/MetricAggregation.java b/jest-common/src/main/java/io/searchbox/core/search/aggregation/MetricAggregation.java
index fa9d3394..d5e8a3b6 100644
--- a/jest-common/src/main/java/io/searchbox/core/search/aggregation/MetricAggregation.java
+++ b/jest-common/src/main/java/io/searchbox/core/search/aggregation/MetricAggregation.java
@@ -247,5 +247,9 @@ public abstract class MetricAggregation extends Aggregation {
     public ValueCountAggregation getValueCountAggregation(String aggName) {
         return jsonRoot.has(aggName) ? new ValueCountAggregation(aggName, jsonRoot.getAsJsonObject(aggName)) : null;
     }
+    
+    public TopHitsAggregation getTopHitsAggregation(String aggName) {
+	return jsonRoot.has(aggName) ? new TopHitsAggregation(aggName, jsonRoot.getAsJsonObject(aggName)) : null;
+    }
 
 }
diff --git a/jest-common/src/main/java/io/searchbox/core/search/aggregation/TopHitsAggregation.java b/jest-common/src/main/java/io/searchbox/core/search/aggregation/TopHitsAggregation.java
new file mode 100644
index 00000000..83344b3c
--- /dev/null
+++ b/jest-common/src/main/java/io/searchbox/core/search/aggregation/TopHitsAggregation.java
@@ -0,0 +1,29 @@
+package io.searchbox.core.search.aggregation;
+
+import java.util.List;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import io.searchbox.core.SearchResult;
+
+public class TopHitsAggregation extends SearchResult {
+
+    protected String name;
+    protected JsonObject jsonRoot;
+    public static final String TYPE = "top_hits";
+
+    public TopHitsAggregation(String name, JsonObject topHitAggregation) {
+	super(new Gson());
+	this.name = name;
+	
+	this.setSucceeded(true);
+	this.setJsonObject(topHitAggregation);
+	//this.setJsonString(topHitAggregation.getAsString());
+	this.setPathToResult("hits/hits/_source");
+    }
+
+    public String getName() {
+	return name;
+    }
+}


Test patch:
diff --git a/jest-common/src/test/java/io/searchbox/search/aggregation/TopHitsAggregationTest.java b/jest-common/src/test/java/io/searchbox/search/aggregation/TopHitsAggregationTest.java
new file mode 100644
index 00000000..b71163fb
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/search/aggregation/TopHitsAggregationTest.java
@@ -0,0 +1,124 @@
+package io.searchbox.search.aggregation;
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParser;
+
+import io.searchbox.core.SearchResult;
+import io.searchbox.core.search.aggregation.TermsAggregation;
+import io.searchbox.core.search.aggregation.TermsAggregation.Entry;
+import io.searchbox.core.search.aggregation.TopHitsAggregation;
+
+public class TopHitsAggregationTest {
+    String json = "{\n" +
+            "    \"_shards\":{\n" +
+            "        \"total\" : 5,\n" +
+            "        \"successful\" : 5,\n" +
+            "        \"failed\" : 0\n" +
+            "    },\n" +
+            "    \"hits\":{\n" +
+            "        \"total\" : 1,\n" +
+            "	     \"hits\": []\n" +
+            "    },\n" +
+            "    \"aggregations\": {\n" +
+            "        \"test\": {\n" +
+            "            \"doc_count_error_upper_bound\": 1,\n" +
+            "            \"sum_other_doc_count\": 2,\n" +
+            "            \"buckets\": [\n" +
+            "                {\n" +
+            "                    \"key\": \"keyTest1\",\n" +
+            "                    \"doc_count\": 2,\n" +
+            "                    \"top_hits_test\": {\n" +
+            "                        \"hits\": {\n" +
+            "                            \"total\": 2,\n" +
+            "                            \"max_score\": 1.22222,\n" +
+            "                            \"hits\": [\n" +
+            "                                {\n" +
+            "                                    \"_index\": \"indexName\",\n" +
+            "                                    \"_type\": \"typeName\",\n" +
+            "                                    \"_id\": \"testId1\",\n" +
+            "                                    \"_score\": 1.11111,\n" +
+            "                                    \"_source\": {\n" +
+            "                                        \"field1\": \"field1Content\"\n" +
+            "                                    }\n" +
+            "                                },\n" +
+            "                                {\n" +
+            "                                    \"_index\": \"indexName\",\n" +
+            "                                    \"_type\": \"typeName\",\n" +
+            "                                    \"_id\": \"testId2\",\n" +
+            "                                    \"_score\": 1.0000,\n" +
+            "                                    \"_source\": {\n" +
+            "                                        \"field1\": \"field2Content\"\n" +
+            "                                    }\n" +
+            "                                }\n" +
+            "                            ]\n" +
+            "                        }\n" +
+            "                    }\n" +
+            "                },\n" +
+            "                {\n" +
+            "                    \"key\": \"keyTest2\",\n" +
+            "                    \"doc_count\": 2,\n" +
+            "                    \"top_hits_test\": {\n" +
+            "                        \"hits\": {\n" +
+            "                            \"total\": 2,\n" +
+            "                            \"max_score\": 1.22222,\n" +
+            "                            \"hits\": [\n" +
+            "                                {\n" +
+            "                                    \"_index\": \"indexName\",\n" +
+            "                                    \"_type\": \"typeName\",\n" +
+            "                                    \"_id\": \"testId21\",\n" +
+            "                                    \"_score\": 1.11111,\n" +
+            "                                    \"_source\": {\n" +
+            "                                        \"field1\": \"field21Content\"\n" +
+            "                                    }\n" +
+            "                                },\n" +
+            "                                {\n" +
+            "                                    \"_index\": \"indexName\",\n" +
+            "                                    \"_type\": \"typeName\",\n" +
+            "                                    \"_id\": \"testId22\",\n" +
+            "                                    \"_score\": 1.0000,\n" +
+            "                                    \"_source\": {\n" +
+            "                                        \"field1\": \"field22Content\"\n" +
+            "                                    }\n" +
+            "                                }\n" +
+            "                            ]\n" +
+            "                        }\n" +
+            "                    }\n" +
+            "                }\n" +
+            "            ]\n" +
+            "        }\n" +
+            "    }\n" + 
+            "}";
+
+    @Test
+    public void testGetMaxScoreWhenMissing() {	
+        SearchResult searchResult = new SearchResult(new Gson());
+        searchResult.setSucceeded(true);
+        searchResult.setJsonString(json);
+        searchResult.setJsonObject(new JsonParser().parse(json).getAsJsonObject());
+        searchResult.setPathToResult("hits/hits/_source");
+
+        Float maxScore = searchResult.getMaxScore();
+        assertNull(maxScore);
+        
+        TermsAggregation termsAgg = searchResult.getAggregations().getTermsAggregation("test");
+
+	assertEquals(new Long(1), termsAgg.getDocCountErrorUpperBound());
+	assertEquals(new Long(2), termsAgg.getSumOtherDocCount());
+
+	List<Entry> termBuckets = termsAgg.getBuckets();
+
+	for (Entry entry : termBuckets) {
+	    TopHitsAggregation topHitAgg = entry.getTopHitsAggregation("top_hits_test");
+
+	    List<String> hits = topHitAgg.getSourceAsStringList();
+
+	    assertEquals(2, hits.size());
+	}
+    }
+}