]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6137 Apply feedback on PR (refactoring of facet processing, tests split)
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 4 Feb 2015 13:25:15 +0000 (14:25 +0100)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 4 Feb 2015 14:14:29 +0000 (15:14 +0100)
server/sonar-server/src/main/java/org/sonar/server/search/FacetValue.java
server/sonar-server/src/main/java/org/sonar/server/search/Facets.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/search/Result.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/search/FacetValueTest.java
server/sonar-server/src/test/java/org/sonar/server/search/FacetsMediumTest.java [new file with mode: 0644]

index ad83b07c3698669972914746813c2dd10e88e5c1..226ea4b57b6f9c8e745f3c55353db9ef6e277d90 100644 (file)
@@ -47,7 +47,11 @@ public class FacetValue {
     }
 
     FacetValue that = (FacetValue) o;
-    return key == null ? that.key == null : key.equals(that.key);
+    if (key == null) {
+      return that.key == null;
+    } else {
+      return key.equals(that.key);
+    }
   }
 
   @Override
@@ -58,8 +62,8 @@ public class FacetValue {
   @Override
   public String toString() {
     return "FacetValue{" +
-      "key='" + key + '\'' +
-      ", value=" + value +
+      "key='" + getKey() + '\'' +
+      ", value=" + getValue() +
       '}';
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/search/Facets.java b/server/sonar-server/src/main/java/org/sonar/server/search/Facets.java
new file mode 100644 (file)
index 0000000..09b99fc
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.search;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.aggregations.Aggregation;
+import org.elasticsearch.search.aggregations.HasAggregations;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram;
+import org.elasticsearch.search.aggregations.bucket.missing.Missing;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+class Facets {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(Facets.class);
+
+  private final Multimap<String, FacetValue> facetValues;
+
+  public Facets(SearchResponse response) {
+    facetValues = LinkedHashMultimap.create();
+
+    if (response.getAggregations() != null) {
+      for (Aggregation facet : response.getAggregations()) {
+        this.processAggregation(facet);
+      }
+    }
+  }
+
+  private void processAggregation(Aggregation aggregation) {
+    if (Missing.class.isAssignableFrom(aggregation.getClass())) {
+      processMissingAggregation(aggregation);
+    } else if (Terms.class.isAssignableFrom(aggregation.getClass())) {
+      processTermsAggregation(aggregation);
+    } else if (HasAggregations.class.isAssignableFrom(aggregation.getClass())) {
+      processSubAggregations(aggregation);
+    } else if (DateHistogram.class.isAssignableFrom(aggregation.getClass())) {
+      processDateHistogram(aggregation);
+    } else {
+      LOGGER.warn("Cannot process {} type of aggregation", aggregation.getClass());
+    }
+  }
+
+  private void processMissingAggregation(Aggregation aggregation) {
+    Missing missing = (Missing) aggregation;
+    long docCount = missing.getDocCount();
+    if (docCount > 0L) {
+      this.facetValues.put(aggregation.getName().replace("_missing", ""), new FacetValue("", docCount));
+    }
+  }
+
+  private void processTermsAggregation(Aggregation aggregation) {
+    Terms termAggregation = (Terms) aggregation;
+    for (Terms.Bucket value : termAggregation.getBuckets()) {
+      String facetName = aggregation.getName();
+      if (facetName.contains("__") && !facetName.startsWith("__")) {
+        facetName = facetName.substring(0, facetName.indexOf("__"));
+      }
+      facetName = facetName.replace("_selected", "");
+      this.facetValues.put(facetName, new FacetValue(value.getKey(), value.getDocCount()));
+    }
+  }
+
+  private void processSubAggregations(Aggregation aggregation) {
+    HasAggregations hasAggregations = (HasAggregations) aggregation;
+    for (Aggregation internalAggregation : hasAggregations.getAggregations()) {
+      this.processAggregation(internalAggregation);
+    }
+  }
+
+  private void processDateHistogram(Aggregation aggregation) {
+    DateHistogram dateHistogram = (DateHistogram) aggregation;
+    for (DateHistogram.Bucket value : dateHistogram.getBuckets()) {
+      this.facetValues.put(dateHistogram.getName(), new FacetValue(value.getKeyAsText().toString(), value.getDocCount()));
+    }
+  }
+
+  public Map<String, Collection<FacetValue>> getFacets() {
+    return this.facetValues.asMap();
+  }
+
+  public Collection<FacetValue> getFacetValues(String facetName) {
+    return this.facetValues.get(facetName);
+  }
+
+  public List<String> getFacetKeys(String facetName) {
+    List<String> keys = new ArrayList<String>();
+    if (this.facetValues.containsKey(facetName)) {
+      for (FacetValue facetValue : facetValues.get(facetName)) {
+        keys.add(facetValue.getKey());
+      }
+    }
+    return keys;
+  }
+
+  @Override
+  public String toString() {
+    return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);
+  }
+}
index a35fab832a1314be45848a2b17fdc40774865a59..55b6faa6e7325a0df445992949994eb5550a35a6 100644 (file)
 package org.sonar.server.search;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.LinkedHashMultimap;
-import com.google.common.collect.Multimap;
 import org.apache.commons.lang.builder.ReflectionToStringBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.search.SearchHit;
-import org.elasticsearch.search.aggregations.Aggregation;
-import org.elasticsearch.search.aggregations.HasAggregations;
-import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram;
-import org.elasticsearch.search.aggregations.bucket.missing.Missing;
-import org.elasticsearch.search.aggregations.bucket.terms.Terms;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
@@ -40,10 +31,8 @@ import java.util.*;
 
 public class Result<K> {
 
-  private static final Logger LOGGER = LoggerFactory.getLogger(Result.class);
-
   private final List<K> hits;
-  private final Multimap<String, FacetValue> facets;
+  private final Facets facets;
   private final long total;
   private final String scrollId;
   private final BaseIndex<K, ?, ?> index;
@@ -55,7 +44,7 @@ public class Result<K> {
   public Result(@Nullable BaseIndex<K, ?, ?> index, SearchResponse response) {
     this.index = index;
     this.scrollId = response.getScrollId();
-    this.facets = LinkedHashMultimap.create();
+    this.facets = new Facets(response);
     this.total = (int) response.getHits().totalHits();
     this.hits = new ArrayList<K>();
     if (index != null) {
@@ -63,59 +52,6 @@ public class Result<K> {
         this.hits.add(index.toDoc(hit.getSource()));
       }
     }
-    if (response.getAggregations() != null) {
-      for (Map.Entry<String, Aggregation> facet : response.getAggregations().asMap().entrySet()) {
-        this.processAggregation(facet.getValue());
-      }
-    }
-  }
-
-  private void processAggregation(Aggregation aggregation) {
-    if (Missing.class.isAssignableFrom(aggregation.getClass())) {
-      processMissingAggregation(aggregation);
-    } else if (Terms.class.isAssignableFrom(aggregation.getClass())) {
-      processTermsAggregation(aggregation);
-    } else if (HasAggregations.class.isAssignableFrom(aggregation.getClass())) {
-      processSubAggregations(aggregation);
-    } else if (DateHistogram.class.isAssignableFrom(aggregation.getClass())) {
-      processDateHistogram(aggregation);
-    } else {
-      LOGGER.warn("Cannot process {} type of aggregation", aggregation.getClass());
-    }
-  }
-
-  private void processMissingAggregation(Aggregation aggregation) {
-    Missing missing = (Missing) aggregation;
-    long docCount = missing.getDocCount();
-    if (docCount > 0L) {
-      this.facets.put(aggregation.getName().replace("_missing",""), new FacetValue("", docCount));
-    }
-  }
-
-  private void processTermsAggregation(Aggregation aggregation) {
-    Terms termAggregation = (Terms) aggregation;
-    for (Terms.Bucket value : termAggregation.getBuckets()) {
-      String facetName = aggregation.getName();
-      if (facetName.contains("__") && !facetName.startsWith("__")) {
-        facetName = facetName.substring(0, facetName.indexOf("__"));
-      }
-      facetName = facetName.replace("_selected", "");
-      this.facets.put(facetName, new FacetValue(value.getKey(), value.getDocCount()));
-    }
-  }
-
-  private void processSubAggregations(Aggregation aggregation) {
-    HasAggregations hasAggregations = (HasAggregations) aggregation;
-    for (Aggregation internalAggregation : hasAggregations.getAggregations()) {
-      this.processAggregation(internalAggregation);
-    }
-  }
-
-  private void processDateHistogram(Aggregation aggregation) {
-    DateHistogram dateHistogram = (DateHistogram) aggregation;
-    for (DateHistogram.Bucket value : dateHistogram.getBuckets()) {
-      this.facets.put(dateHistogram.getName(), new FacetValue(value.getKeyAsText().toString(), value.getDocCount()));
-    }
   }
 
   public Iterator<K> scroll() {
@@ -132,24 +68,17 @@ public class Result<K> {
   }
 
   public Map<String, Collection<FacetValue>> getFacets() {
-    return this.facets.asMap();
+    return this.facets.getFacets();
   }
 
   @CheckForNull
   public Collection<FacetValue> getFacetValues(String facetName) {
-    return this.facets.get(facetName);
+    return this.facets.getFacetValues(facetName);
   }
 
   @CheckForNull
   public List<String> getFacetKeys(String facetName) {
-    if (this.facets.containsKey(facetName)) {
-      List<String> keys = new ArrayList<String>();
-      for (FacetValue facetValue : facets.get(facetName)) {
-        keys.add(facetValue.getKey());
-      }
-      return keys;
-    }
-    return null;
+    return this.facets.getFacetKeys(facetName);
   }
 
   @Override
index c56a971ae2ace4c1ec829000959872297b85b716..9da928b740f8c19710f7e5e7806be1393a82fae8 100644 (file)
@@ -652,24 +652,9 @@ public class IssueIndexMediumTest {
   }
 
   @Test
-  public void facet_on_created_at() throws Exception {
+  public void facet_on_created_at_with_less_than_20_days() throws Exception {
 
-    ComponentDto project = ComponentTesting.newProjectDto();
-    ComponentDto file = ComponentTesting.newFileDto(project);
-
-    TimeZone.setDefault(TimeZone.getTimeZone("GMT+04:00"));
-
-    IssueDoc issue0 = IssueTesting.newDoc("ISSUE0", file).setFuncCreationDate(DateUtils.parseDateTime("2011-04-25T01:05:13+0400"));
-    IssueDoc issue1 = IssueTesting.newDoc("ISSUE1", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-01T12:34:56+0400"));
-    IssueDoc issue2 = IssueTesting.newDoc("ISSUE2", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-01T23:45:60+0400"));
-    IssueDoc issue3 = IssueTesting.newDoc("ISSUE3", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-02T12:34:56+0400"));
-    IssueDoc issue4 = IssueTesting.newDoc("ISSUE4", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-05T12:34:56+0400"));
-    IssueDoc issue5 = IssueTesting.newDoc("ISSUE5", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-20T12:34:56+0400"));
-    IssueDoc issue6 = IssueTesting.newDoc("ISSUE6", file).setFuncCreationDate(DateUtils.parseDateTime("2015-01-18T12:34:56+0400"));
-
-    indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6);
-
-    QueryContext queryContext = new QueryContext().addFacets(Arrays.asList("createdAt"));
+    QueryContext queryContext = fixtureForCreatedAtFacet();
 
     Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2014-09-08")).build(),
       queryContext).getFacets().get("createdAt");
@@ -680,16 +665,28 @@ public class IssueIndexMediumTest {
         new FacetValue("2014-09-03T04:00:00+0000", 0),
         new FacetValue("2014-09-04T04:00:00+0000", 0),
         new FacetValue("2014-09-05T04:00:00+0000", 1));
+  }
+
+  @Test
+  public void facet_on_created_at_with_less_than_20_weeks() throws Exception {
+
+    QueryContext queryContext = fixtureForCreatedAtFacet();
 
-    createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2014-09-21")).build(),
+    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2014-09-21")).build(),
       queryContext).getFacets().get("createdAt");
     assertThat(createdAt).hasSize(3)
       .containsOnly(
         new FacetValue("2014-09-01T04:00:00+0000", 4),
         new FacetValue("2014-09-08T04:00:00+0000", 0),
         new FacetValue("2014-09-15T04:00:00+0000", 1));
+  }
+
+  @Test
+  public void facet_on_created_at_with_less_than_20_months() throws Exception {
 
-    createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2015-01-19")).build(),
+    QueryContext queryContext = fixtureForCreatedAtFacet();
+
+    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2015-01-19")).build(),
       queryContext).getFacets().get("createdAt");
     assertThat(createdAt).hasSize(5)
       .containsOnly(
@@ -698,8 +695,14 @@ public class IssueIndexMediumTest {
         new FacetValue("2014-11-01T04:00:00+0000", 0),
         new FacetValue("2014-12-01T04:00:00+0000", 0),
         new FacetValue("2015-01-01T04:00:00+0000", 1));
+  }
 
-    createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2011-01-01")).createdBefore(DateUtils.parseDate("2016-01-01")).build(),
+  @Test
+  public void facet_on_created_at_with_more_than_20_months() throws Exception {
+
+    QueryContext queryContext = fixtureForCreatedAtFacet();
+
+    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2011-01-01")).createdBefore(DateUtils.parseDate("2016-01-01")).build(),
       queryContext).getFacets().get("createdAt");
     assertThat(createdAt).hasSize(5)
       .containsOnly(
@@ -709,8 +712,14 @@ public class IssueIndexMediumTest {
         new FacetValue("2014-01-01T04:00:00+0000", 5),
         new FacetValue("2015-01-01T04:00:00+0000", 1));
 
-    // createdAfter not set: taking min value
-    createdAt = index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2016-01-01")).build(),
+  }
+
+  @Test
+  public void facet_on_created_at_without_start_bound() throws Exception {
+
+    QueryContext queryContext = fixtureForCreatedAtFacet();
+
+    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2016-01-01")).build(),
       queryContext).getFacets().get("createdAt");
     assertThat(createdAt).hasSize(5)
       .containsOnly(
@@ -721,6 +730,26 @@ public class IssueIndexMediumTest {
         new FacetValue("2015-01-01T04:00:00+0000", 1));
   }
 
+  protected QueryContext fixtureForCreatedAtFacet() {
+    ComponentDto project = ComponentTesting.newProjectDto();
+    ComponentDto file = ComponentTesting.newFileDto(project);
+
+    TimeZone.setDefault(TimeZone.getTimeZone("GMT+04:00"));
+
+    IssueDoc issue0 = IssueTesting.newDoc("ISSUE0", file).setFuncCreationDate(DateUtils.parseDateTime("2011-04-25T01:05:13+0400"));
+    IssueDoc issue1 = IssueTesting.newDoc("ISSUE1", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-01T12:34:56+0400"));
+    IssueDoc issue2 = IssueTesting.newDoc("ISSUE2", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-01T23:45:60+0400"));
+    IssueDoc issue3 = IssueTesting.newDoc("ISSUE3", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-02T12:34:56+0400"));
+    IssueDoc issue4 = IssueTesting.newDoc("ISSUE4", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-05T12:34:56+0400"));
+    IssueDoc issue5 = IssueTesting.newDoc("ISSUE5", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-20T12:34:56+0400"));
+    IssueDoc issue6 = IssueTesting.newDoc("ISSUE6", file).setFuncCreationDate(DateUtils.parseDateTime("2015-01-18T12:34:56+0400"));
+
+    indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6);
+
+    QueryContext queryContext = new QueryContext().addFacets(Arrays.asList("createdAt"));
+    return queryContext;
+  }
+
   @Test
   public void paging() throws Exception {
     ComponentDto project = ComponentTesting.newProjectDto();
index 8b066698e8b34e6eb2c0f118a9818b01b2db88af..dd3ad35449db3c7bf379829cb34a30c6085b39d9 100644 (file)
@@ -39,5 +39,18 @@ public class FacetValueTest {
     assertThat(facetValue.equals(withNullKey)).isFalse();
     assertThat(withNullKey.equals(withNullKey)).isTrue();
     assertThat(withNullKey.equals(facetValue)).isFalse();
+    assertThat(withNullKey.equals(new FacetValue(null, 666))).isTrue();
+  }
+
+  @Test
+  public void should_use_key_hashcode() {
+    assertThat(new FacetValue(null, 42).hashCode()).isZero();
+    String key = "polop";
+    assertThat(new FacetValue(key, 666).hashCode()).isEqualTo(key.hashCode());
+  }
+
+  @Test
+  public void should_define_toString() {
+    assertThat(new FacetValue("polop", 42).toString()).isEqualTo("FacetValue{key='polop', value=42}");
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/search/FacetsMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/search/FacetsMediumTest.java
new file mode 100644 (file)
index 0000000..5d99924
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.search;
+
+import com.google.common.collect.ImmutableMap;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram.Interval;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.NewIndex.NewIndexType;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class FacetsMediumTest {
+
+  private static final String INDEX = "facetstests";
+  private static final String TYPE = "tagsdoc";
+  private static final String FIELD_KEY = "key";
+  private static final String FIELD_TAGS = "tags";
+  private static final String FIELD_CREATED_AT = "createdAt";
+
+  @Rule
+  public EsTester esTester = new EsTester().addDefinitions(new FacetsTestDefinition());
+
+  @Test
+  public void should_ignore_result_without_aggregations() throws Exception {
+    Facets facets = new Facets(mock(SearchResponse.class));
+    assertThat(facets.getFacets()).isEmpty();
+    assertThat(facets.getFacetKeys("polop")).isEmpty();
+    assertThat(facets.getFacetValues("polop")).isEmpty();
+  }
+
+  @Test
+  public void should_ignore_unknown_aggregation_type() throws Exception {
+    esTester.putDocuments(INDEX, TYPE,
+      newTagsDocument("noTags"),
+      newTagsDocument("oneTag", "tag1"),
+      newTagsDocument("twoTags", "tag1", "tag2"),
+      newTagsDocument("twoTags", "tag1", "tag2", "tag3"),
+      newTagsDocument("twoTags", "tag1", "tag2", "tag3", "tag4"));
+    SearchRequestBuilder search = esTester.client().prepareSearch(INDEX).setTypes(TYPE)
+      .addAggregation(AggregationBuilders.cardinality(FIELD_TAGS).field(FIELD_TAGS));
+
+    Facets facets = new Facets(search.get());
+    assertThat(facets.getFacets()).isEmpty();
+    assertThat(facets.getFacetKeys(FIELD_TAGS)).isEmpty();
+  }
+
+  @Test
+  public void should_process_result_with_nested_missing_and_terms_aggregations() throws Exception {
+    esTester.putDocuments(INDEX, TYPE,
+      newTagsDocument("noTags"),
+      newTagsDocument("oneTag", "tag1"),
+      newTagsDocument("twoTags", "tag1", "tag2"),
+      newTagsDocument("twoTags", "tag1", "tag2", "tag3"),
+      newTagsDocument("twoTags", "tag1", "tag2", "tag3", "tag4"));
+
+    SearchRequestBuilder search = esTester.client().prepareSearch(INDEX).setTypes(TYPE)
+      .addAggregation(AggregationBuilders.global("tags__global")
+        .subAggregation(AggregationBuilders.missing("tags_missing").field(FIELD_TAGS))
+        .subAggregation(AggregationBuilders.terms("tags").field(FIELD_TAGS).size(2))
+        .subAggregation(AggregationBuilders.terms("tags__selected").field(FIELD_TAGS).include("tag4"))
+        .subAggregation(AggregationBuilders.terms("__ignored").field(FIELD_TAGS).include("tag3")));
+
+    Facets facets = new Facets(search.get());
+    assertThat(facets.getFacets()).isNotEmpty();
+    assertThat(facets.getFacetKeys(FIELD_TAGS)).containsOnly("", "tag1", "tag2", "tag4");
+    assertThat(facets.getFacetKeys(FIELD_CREATED_AT)).isEmpty();
+    assertThat(facets.toString()).isEqualTo("{"
+      + "tags=["
+        + "FacetValue{key='tag1', value=2}, "
+        + "FacetValue{key='tag2', value=1}, "
+        + "FacetValue{key='tag4', value=1}, "
+        + "FacetValue{key='', value=1}"
+      + "], "
+      + "__ignored=[FacetValue{key='tag3', value=1}]"
+      + "}");
+  }
+
+  @Test
+  public void should_ignore_empty_missing_aggregation() throws Exception {
+    esTester.putDocuments(INDEX, TYPE,
+      newTagsDocument("oneTag", "tag1"),
+      newTagsDocument("twoTags", "tag1", "tag2"),
+      newTagsDocument("twoTags", "tag1", "tag2", "tag3"),
+      newTagsDocument("twoTags", "tag1", "tag2", "tag3", "tag4"));
+
+    SearchRequestBuilder search = esTester.client().prepareSearch(INDEX).setTypes(TYPE)
+      .addAggregation(AggregationBuilders.global("tags__global")
+        .subAggregation(AggregationBuilders.missing("tags_missing").field(FIELD_TAGS))
+        .subAggregation(AggregationBuilders.terms("tags").field(FIELD_TAGS).size(2))
+        .subAggregation(AggregationBuilders.terms("tags__selected").field(FIELD_TAGS).include("tag4"))
+        .subAggregation(AggregationBuilders.terms("__ignored").field(FIELD_TAGS).include("tag3")));
+
+    Facets facets = new Facets(search.get());
+    assertThat(facets.getFacets()).isNotEmpty();
+    assertThat(facets.getFacetKeys(FIELD_TAGS)).containsOnly("tag1", "tag2", "tag4");
+    assertThat(facets.getFacetKeys(FIELD_CREATED_AT)).isEmpty();
+  }
+
+  @Test
+  public void should_process_result_with_date_histogram() throws Exception {
+    esTester.putDocuments(INDEX, TYPE,
+      newTagsDocument("first"), newTagsDocument("second"), newTagsDocument("third"));
+
+    SearchRequestBuilder search = esTester.client().prepareSearch(INDEX).setTypes(TYPE)
+      .addAggregation(
+        AggregationBuilders.dateHistogram(FIELD_CREATED_AT)
+          .field(FIELD_CREATED_AT)
+          .interval(Interval.MINUTE)
+          .format(DateUtils.DATETIME_FORMAT));
+
+    Facets facets = new Facets(search.get());
+    assertThat(facets.getFacets()).isNotEmpty();
+    assertThat(facets.getFacetKeys(FIELD_TAGS)).isEmpty();
+    assertThat(facets.getFacetKeys(FIELD_CREATED_AT)).hasSize(1);
+    FacetValue value = facets.getFacetValues(FIELD_CREATED_AT).iterator().next();
+    assertThat(DateUtils.parseDateTime(value.getKey()).before(new Date()));
+    assertThat(value.getValue()).isEqualTo(3L);
+  }
+
+  private static Map<String, Object> newTagsDocument(String key, String... tags) {
+    ImmutableMap<String, Object> doc = ImmutableMap.<String, Object>of(
+      FIELD_KEY, key,
+      FIELD_TAGS, Arrays.asList(tags),
+      FIELD_CREATED_AT, new Date());
+    return doc;
+  }
+
+  static class FacetsTestDefinition implements org.sonar.server.es.IndexDefinition {
+
+    @Override
+    public void define(IndexDefinitionContext context) {
+      NewIndexType newType = context.create(INDEX).createType(TYPE);
+      newType.setAttribute("_id", ImmutableMap.of("path", FIELD_KEY));
+      newType.stringFieldBuilder(FIELD_KEY).build();
+      newType.stringFieldBuilder(FIELD_TAGS).build();
+      newType.createDateTimeField(FIELD_CREATED_AT);
+    }
+  }
+}