Browse Source

HUE-2173 [search] Core of Analytics facets

Romain Rigaux 10 năm trước cách đây
mục cha
commit
c9ee330

+ 6 - 0
apps/search/src/search/conf.py

@@ -35,3 +35,9 @@ SECURITY_ENABLED = Config(
   help=_("Whether Solr requires client to perform Kerberos authentication."),
   default=False,
   type=coerce_bool)
+
+LATEST = Config(
+  key="latest",
+  help=_("Use latest Solr 5.2+ features."),
+  default=False,
+  type=coerce_bool)

+ 57 - 27
apps/search/src/search/models.py

@@ -713,40 +713,27 @@ def augment_solr_response(response, collection, query):
     for facet in collection['facets']:
       category = facet['type']
       name = NAME % facet
-      
+
       if category == 'function' and name in response['facets']:
         value = response['facets'][name]
         collection_facet = get_facet_field(category, name, collection['facets'])
-        if collection_facet:
-          facet = {
-            'id': collection_facet['id'],
-            'query': name,
-            'type': category,
-            'label': name,
-            'counts': value,
-          }
-          normalized_facets.append(facet)
-        else:
-          print name, value
-      elif category == 'terms' and name in response['facets']:
+        facet = {
+          'id': collection_facet['id'],
+          'query': name,
+          'type': category,
+          'label': name,
+          'counts': value,
+        }
+        normalized_facets.append(facet)
+      elif category == 'nested' and name in response['facets']:
         value = response['facets'][name]
         collection_facet = get_facet_field(category, name, collection['facets'])
 
-        # none empty
-
-        countss = []
-        buckets = []
-        print response['facets'][name]['buckets']
-        for bucket in response['facets'][name]['buckets']:          
-          buckets.append(bucket['val'])
-          if 'd2' in bucket:
-            buckets.append(bucket['d2'])
-          else:
-            buckets.append(bucket['count'])
+        counts = _augment_stats_2d(name, facet, response['facets'][name]['buckets'], selected_values)
 
-        counts = pairwise2(facet['field'], selected_values.get(facet['id'], []), buckets)
         if collection_facet['properties']['sort'] == 'asc':
           counts.reverse()
+
         facet = {
           'id': collection_facet['id'],
           'field': facet['field'],
@@ -754,12 +741,13 @@ def augment_solr_response(response, collection, query):
           'label': collection_facet['label'],
           'counts': counts,
         }
-        print facet['counts']
+
         normalized_facets.append(facet)
 
     # Remove unnecessary facet data
     if response:
       response.pop('facet_counts')
+      response.pop('facets')
 
   # HTML escaping
   for doc in response['response']['docs']:
@@ -835,8 +823,50 @@ def _augment_pivot_2d(name, facet_id, counts, selected_values):
   return augmented
 
 
-def _augment_pivot_nd(facet_id, counts, selected_values, fields='', values=''):
+def _augment_stats_2d(name, facet, counts, selected_values):
+  fq_fields = []
+  fq_values = []
+  fq_filter = []
+  _selected_values = []
+  _fields = [facet['field']] + [facet['field'] for facet in facet['properties']['facets']]
+
+  return __augment_stats_2d(counts, facet['field'], fq_fields, fq_values, fq_filter, _selected_values, _fields)
+
+
+def __augment_stats_2d(counts, label, fq_fields, fq_values, fq_filter, _selected_values, _fields):
+  augmented = []
+
+  for bucket in counts:
+    val = bucket['val']
+    count = bucket['count']
+
+    _fq_fields = fq_fields + _fields[0:1]
+    _fq_values = fq_values + [val]
 
+    if 'd2' in bucket:
+      if type(bucket['d2']) == dict:
+        augmented += __augment_stats_2d(bucket['d2']['buckets'], val, _fq_fields, _fq_values, fq_filter, _selected_values, _fields[1:])
+      else:
+        augmented.append(_get_augmented(bucket['d2'], val, label, _fq_values, _fq_fields, fq_filter, _selected_values))
+    else:
+      augmented.append(_get_augmented(count, val, label, _fq_values, _fq_fields, fq_filter, _selected_values))
+
+  return augmented
+
+
+def _get_augmented(count, val, label, fq_values, fq_fields, fq_filter, _selected_values):
+    return {
+        "count": count,
+        "value": val,
+        "cat": label,
+        'selected': fq_values in _selected_values,
+        'exclude': all([f['exclude'] for f in fq_filter if f['value'] == val]),
+        'fq_fields': fq_fields,
+        'fq_values': fq_values,
+    }
+
+
+def _augment_pivot_nd(facet_id, counts, selected_values, fields='', values=''):
   for c in counts:
     fq_fields = (fields if fields else []) + [c['field']]
     fq_values = (values if values else []) + [smart_str(c['value'])]

+ 13 - 8
apps/search/src/search/static/search/js/search.ko.js

@@ -483,8 +483,8 @@ var Collection = function (vm, collection) {
         vm.search();
       });
     }
-    if (facet.properties.function) {
-      facet.properties.function.subscribe(function () {
+    if (facet.properties.aggregate) {
+      facet.properties.aggregate.subscribe(function () {
         vm.search();
       });
     }
@@ -523,8 +523,8 @@ var Collection = function (vm, collection) {
               vm.search();
             });
           }
-          if (facet.properties.function) {
-            facet.properties.function.subscribe(function () {
+          if (facet.properties.aggregate) {
+            facet.properties.aggregate.subscribe(function () {
               vm.search();
             });
           }
@@ -544,28 +544,31 @@ var Collection = function (vm, collection) {
         'field': facet.properties.facets_form.field,
         'limit': facet.properties.facets_form.limit,
         'mincount': facet.properties.facets_form.mincount,
-        'functionz': facet.properties.facets_form.function,
+        'aggregate': facet.properties.facets_form.aggregate,
       });
       facet.properties.facets_form.field = null;
       facet.properties.facets_form.limit = 5;
       facet.properties.facets_form.mincount = 1;
-      facet.properties.facets_form.function = 'count';
+      facet.properties.facets_form.aggregate = 'count';
     } else {
       if (typeof facet.properties.facets_form.field != 'undefined') {
         pivot = ko.mapping.fromJS({
           'field': facet.properties.facets_form.field(),
           'limit': facet.properties.facets_form.limit(),
           'mincount': facet.properties.facets_form.mincount(),
-          'functionz': facet.properties.facets_form.function()
+          'aggregate': facet.properties.facets_form.aggregate ? facet.properties.facets_form.aggregate() : ''
         });
         facet.properties.facets_form.field(null);
         facet.properties.facets_form.limit(5);
         facet.properties.facets_form.mincount(1);
-        facet.properties.facets_form.function('count');
+        facet.properties.facets_form.aggregate ? facet.properties.facets_form.aggregate('count') : '';
       }
     }
 
     if (pivot != null) {
+      pivot.aggregate.subscribe(function() {
+        vm.search();
+      });
       facet.properties.facets.push(pivot);
       vm.search();
     }
@@ -974,6 +977,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
 
   self.intervalOptions = ko.observableArray(ko.bindingHandlers.daterangepicker.INTERVAL_OPTIONS);
   self.isNested = ko.observable(false);
+  self.isLatest = ko.mapping.fromJS(typeof initial_json.is_latest != "undefined" ? initial_json.is_latest : false);
 
   // Models
   self.collection = new Collection(self, collection_json.collection);
@@ -1059,6 +1063,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
   self.draggableTree = ko.observable(bareWidgetBuilder("Tree", "tree-widget"));
   self.draggableHeatmap = ko.observable(bareWidgetBuilder("Heatmap", "heatmap-widget"));
   self.draggableCounter = ko.observable(bareWidgetBuilder("Counter", "hit-widget"));
+  self.draggableBucket = ko.observable(bareWidgetBuilder("Histogram", "bucket-widget"));
 
   self.availableDateFields = ko.computed(function() {
     return $.grep(self.collection.availableFacetFields(), function(field) { return DATE_TYPES.indexOf(field.type()) != -1; });

+ 103 - 49
apps/search/src/search/templates/search.mako

@@ -145,7 +145,8 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
          </a>
     </div>
 
-    <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableNumbers() },
+    <div data-bind="visible: $root.isLatest,
+                    css: { 'draggable-widget': true, 'disabled': !availableDraggableNumbers() },
                     draggable: {data: draggableCounter(), isEnabled: availableDraggableNumbers,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
                               'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"
@@ -186,7 +187,8 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
                        <i class="hcha hcha-pie-chart"></i>
          </a>
     </div>
-    <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableChart() },
+    <div data-bind="visible: ! $root.isLatest(),
+                    css: { 'draggable-widget': true, 'disabled': !availableDraggableChart() },
                     draggable: {data: draggableBar(), isEnabled: availableDraggableChart,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
                               'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"
@@ -195,7 +197,8 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
                        <i class="hcha hcha-bar-chart"></i>
          </a>
     </div>
-    <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableNumbers() },
+    <div data-bind="visible: ! $root.isLatest(),
+                    css: { 'draggable-widget': true, 'disabled': !availableDraggableNumbers() },
                     draggable: {data: draggableLine(), isEnabled: availableDraggableNumbers,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
                               'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"
@@ -204,6 +207,16 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
                        <i class="hcha hcha-line-chart"></i>
          </a>
     </div>
+    <div data-bind="visible: $root.isLatest(),
+                    css: { 'draggable-widget': true, 'disabled': ! availableDraggableChart() },
+                    draggable: {data: draggableBucket(), isEnabled: availableDraggableChart,
+                    options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
+                              'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"
+         title="${_('Histogram Chart')}" rel="tooltip" data-placement="top">
+         <a data-bind="style: { cursor: $root.availableDraggableChart() ? 'move' : 'default' }">
+                       <i class="hcha hcha-bar-chart"></i>
+         </a>
+    </div>
     <div data-bind="css: { 'draggable-widget': true, 'disabled': false },
                     draggable: {data: draggableTree(), isEnabled: true,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
@@ -213,7 +226,8 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
                        <i class="fa fa-sitemap fa-rotate-270"></i>
          </a>
     </div>
-    <div data-bind="css: { 'draggable-widget': true, 'disabled': false },
+    <div data-bind="visible: ! $root.isLatest(),
+                    css: { 'draggable-widget': true, 'disabled': false },
                     draggable: {data: draggableHeatmap(), isEnabled: true,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
                               'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"
@@ -296,7 +310,7 @@ ${ dashboard.layout_skeleton() }
       </div>
     <!-- /ko -->
 
-    <!-- ko if: type() == 'range' || type() == 'range-up' || (type() == 'terms' && properties.min)-->
+    <!-- ko if: type() == 'range' || type() == 'range-up' || (type() == 'nested' && typeof properties.min != "undefined")-->
       <!-- ko ifnot: properties.isDate() -->
         <div class="slider-cnt" data-bind="slider: {start: properties.min, end: properties.max, gap: properties.initial_gap, min: properties.initial_start, max: properties.initial_end, properties: properties, labels: SLIDER_LABELS}"></div>
       <!-- /ko -->
@@ -306,7 +320,7 @@ ${ dashboard.layout_skeleton() }
       <!-- /ko -->
     <!-- /ko -->
 
-    <!-- ko if: type() == 'field' || type() == 'terms' -->
+    <!-- ko if: type() == 'field' -->
       <div class="facet-field-cnt">
         <span class="spinedit-cnt">
           <span class="facet-field-label facet-field-label-fixed-width">
@@ -332,8 +346,8 @@ ${ dashboard.layout_skeleton() }
     <!-- /ko -->
     </div>
 
-    <!-- ko if: type() == 'pivot' || type() == 'terms' -->
-      <div class="facet-field-tile" data-bind="visible: properties.scope() == 'tree' || properties.facets().length == 0">
+    <!-- ko if: type() == 'pivot' || type() == 'nested' -->
+      <div class="facet-field-tile" data-bind="visible: properties.scope() == 'tree' || (type() == 'pivot' && properties.facets().length == 0) || (type() == 'nested' && properties.facets().length < 3)">
         <div class="facet-field-cnt">
           <span class="facet-field-label facet-field-label-fixed-width facet-field-label-fixed-width-double facet-field-label-title">
             ${ _('Add a dimension') }
@@ -349,16 +363,11 @@ ${ dashboard.layout_skeleton() }
           </span>
         </div>
 
-     <span class="facet-field-label">${ _('Metric') }</span>
-      <select data-bind="value: properties.facets_form.function">
-        <option value="unique" selected="selected" label="${ _('Unique Count') }">${ _('Unique Count') }</option>
-        <option value="avg" label="${ _('Average') }">${ _('Average') }</option>
-        <option value="sum" label="${ _('Sum') }">${ _('Sum') }</option>
-        <option value="min" label="${ _('Min') }">${ _('Min') }</option>
-        <option value="max" label="${ _('Max') }">${ _('Max') }</option>
-        <option value="sumsq" label="${ _('Sum of square') }">${ _('Sum of square') }</option>
-        <option value="median" label="${ _('Median') }">${ _('Median') }</option>
-      </select>
+        <!-- ko if: type() == 'nested' -->
+          <span class="facet-field-label">${ _('Metric') }</span>
+          <select data-bind="options: HIT_OPTIONS, optionsText: 'label', optionsValue: 'value', value: properties.facets_form.aggregate">
+          </select>
+        <!-- /ko -->
 
         <div class="facet-field-cnt">
           <span class="spinedit-cnt">
@@ -820,23 +829,70 @@ ${ dashboard.layout_skeleton() }
       <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut"><i class="fa fa-search-minus"></i> ${ _('reset') }</a>
       <span class="facet-field-label" data-bind="visible: $root.query.multiqs().length > 1">${ _('Group by') }</span>
       <select class="input-medium" data-bind="visible: $root.query.multiqs().length > 1, options: $root.query.multiqs, optionsValue: 'id', optionsText: 'label', value: $root.query.selectedMultiq"></select>
+    </div>
+
+    <!-- ko if: $root.collection.getFacetById($parent.id()) -->
+      <div data-bind="timelineChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(), transformer: timelineChartDataTransformer,
+        type: $root.collection.getFacetById($parent.id()).properties.timelineChartType,
+        fqs: $root.query.fqs,
+        onSelectRange: function(from, to){ $root.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()}) },
+        onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
+        onClick: function(d){ $root.query.selectRangeFacet({count: d.obj.value, widget_id: $parent.id(), from: d.obj.from, to: d.obj.to, cat: d.obj.field}) },
+        onComplete: function(){ $root.getWidgetById($parent.id()).isLoading(false) }}" />
+    <!-- /ko -->
+  </div>
+  <!-- /ko -->
+</script>
+
 
+<script type="text/html" id="bar-widget">
+  <div class="widget-spinner" data-bind="visible: isLoading()">
+    <!--[if !IE]> --><i class="fa fa-spinner fa-spin"></i><!-- <![endif]-->
+    <!--[if IE]><img src="${ static('desktop/art/spinner.gif') }" /><![endif]-->
+  </div>
+
+  <!-- ko if: $root.getFacetFromQuery(id()).has_data() -->
+  <div class="row-fluid" data-bind="with: $root.getFacetFromQuery(id())">
+    <div data-bind="visible: $root.isEditing, with: $root.collection.getFacetById($parent.id())" style="margin-bottom: 20px">
+      <span data-bind="template: { name: 'facet-toggle' }">
+      </span>
+    </div>
+
+    <div data-bind="with: $root.collection.getFacetById($parent.id())">
+      <!-- ko if: properties.canRange -->
+        <div style="padding-bottom: 10px; text-align: right; padding-right: 20px">
+          <span class="facet-field-label">${ _('Zoom') }</span>
+          <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut"><i class="fa fa-search-minus"></i> ${ _('reset') }</a>
+        </div>
+      <!-- /ko -->
     </div>
+
     <!-- ko if: $root.collection.getFacetById($parent.id()) -->
-    <div data-bind="timelineChart: {datum: {counts: counts(), extraSeries: extraSeries(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(), transformer: timelineChartDataTransformer,
-      type: $root.collection.getFacetById($parent.id()).properties.timelineChartType,
+    <div data-bind="barChart: {datum: {counts: counts(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(),
       fqs: $root.query.fqs,
-      onSelectRange: function(from, to){ $root.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()}) },
+      transformer: ($data.type == 'range-up' ? barChartRangeUpDataTransformer : barChartDataTransformer),
       onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
-      onClick: function(d){ $root.query.selectRangeFacet({count: d.obj.value, widget_id: $parent.id(), from: d.obj.from, to: d.obj.to, cat: d.obj.field}) },
-      onComplete: function(){ $root.getWidgetById($parent.id()).isLoading(false) }}" />
+      onClick: function(d) {
+        if (d.obj.field != undefined) {
+          if ($data.type == 'range-up') {
+            viewModel.query.selectRangeUpFacet({count: d.obj.value, widget_id: d.obj.widget_id, from: d.obj.from, to: d.obj.to, cat: d.obj.field, 'exclude': false, is_up: d.obj.is_up});
+          } else {
+            viewModel.query.selectRangeFacet({count: d.obj.value, widget_id: d.obj.widget_id, from: d.obj.from, to: d.obj.to, cat: d.obj.field});
+          }
+        } else {
+          viewModel.query.toggleFacet({facet: d.obj, widget_id: d.obj.widget_id});
+        }
+      },
+      onSelectRange: function(from, to){ viewModel.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()}) },
+      onComplete: function(){ viewModel.getWidgetById($parent.id()).isLoading(false) } }"
+    />
     <!-- /ko -->
   </div>
   <!-- /ko -->
 </script>
 
 
-<script type="text/html" id="bar-widget">
+<script type="text/html" id="bucket-widget">
   <div class="widget-spinner" data-bind="visible: isLoading()">
     <!--[if !IE]> --><i class="fa fa-spinner fa-spin"></i><!-- <![endif]-->
     <!--[if IE]><img src="${ static('desktop/art/spinner.gif') }" /><![endif]-->
@@ -850,28 +906,33 @@ ${ dashboard.layout_skeleton() }
     </div>
 
     <div data-bind="with: $root.collection.getFacetById($parent.id())">
-      <!-- ko if: type() == 'range' || type() == 'range-up' -->
+      <!-- ko if: properties.canRange -->
         <div style="padding-bottom: 10px; text-align: right; padding-right: 20px">
           <span class="facet-field-label">${ _('Zoom') }</span>
           <a href="javascript:void(0)" data-bind="click: $root.collection.rangeZoomOut"><i class="fa fa-search-minus"></i> ${ _('reset') }</a>
         </div>
       <!-- /ko -->
-      
-<div class="dimensions-header margin-bottom-10" data-bind="visible: $root.isEditing() && $data.properties.facets().length > 0">
+
+      <div class="dimensions-header margin-bottom-10" data-bind="visible: $root.isEditing() && properties.facets().length > 0">
         <span class="muted">${ _('Selected dimensions') }</span>
       </div>
-      <div data-bind="foreach: $data.properties.facets, visible: $root.isEditing">
+
+      <div data-bind="foreach: properties.facets, visible: $root.isEditing">
         <div class="filter-box">
           <div class="title">
             <a data-bind="click: function() { $root.collection.removePivotFacetValue({'pivot_facet': $parent, 'value': $data}); }" class="pull-right" href="javascript:void(0)">
               <i class="fa fa-times"></i>
             </a>
             <span data-bind="text: field"></span>
-            <span data-bind="text: functionz"></span>
             &nbsp;
           </div>
 
           <div class="content">
+            <div class="facet-field-cnt">
+              <span class="facet-field-label">${ _('Metric') }</span>
+              <select data-bind="options: HIT_OPTIONS, optionsText: 'label', optionsValue: 'value', value: aggregate"></select>
+            </div>
+
             <div class="facet-field-cnt">
               <span class="spinedit-cnt">
                 <span class="facet-field-label facet-field-label-fixed-width">
@@ -892,28 +953,19 @@ ${ dashboard.layout_skeleton() }
           </div>
         </div>
       </div>
-      <div class="clearfix"></div>      
+      <div class="clearfix"></div>
     </div>
 
     <!-- ko if: $root.collection.getFacetById($parent.id()) -->
-    <div data-bind="barChart: {datum: {counts: counts(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(), field: field, label: label(),
+    <div data-bind="barChart: {datum: {counts: counts(), widget_id: $parent.id(), label: label()}, stacked: $root.collection.getFacetById($parent.id()).properties.stacked(),
+      isPivot: true,
       fqs: $root.query.fqs,
-      transformer: ($data.type == 'range-up' ? barChartRangeUpDataTransformer : barChartDataTransformer),
+      transformer: pivotChartDataTransformer,
       onStateChange: function(state){ $root.collection.getFacetById($parent.id()).properties.stacked(state.stacked); },
       onClick: function(d) {
-        if (d.obj.field != undefined) {
-          if ($data.type == 'range-up') {
-            viewModel.query.selectRangeUpFacet({count: d.obj.value, widget_id: d.obj.widget_id, from: d.obj.from, to: d.obj.to, cat: d.obj.field, 'exclude': false, is_up: d.obj.is_up});
-          } else {
-            viewModel.query.selectRangeFacet({count: d.obj.value, widget_id: d.obj.widget_id, from: d.obj.from, to: d.obj.to, cat: d.obj.field});
-          }
-        } else {
-          viewModel.query.toggleFacet({facet: d.obj, widget_id: d.obj.widget_id});
-        }
+        $root.query.togglePivotFacet({facet: d.obj, widget_id: id()});
       },
-      onSelectRange: function(from, to){ viewModel.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()}) },
-      onComplete: function(){ viewModel.getWidgetById($parent.id()).isLoading(false) } }"
-    />
+      onComplete: function(){ viewModel.getWidgetById($parent.id()).isLoading(false) } }" />
     <!-- /ko -->
   </div>
   <!-- /ko -->
@@ -1033,6 +1085,7 @@ ${ dashboard.layout_skeleton() }
           </div>
         </div>
       </div>
+
       <div class="clearfix"></div>
 
       <!-- ko if: properties.scope() == 'tree' -->
@@ -1047,7 +1100,6 @@ ${ dashboard.layout_skeleton() }
           onComplete: function(){ viewModel.getWidgetById($parent.id()).isLoading(false) } }"
         />
       <!-- /ko -->
-
     </div>
   </div>
   <!-- /ko -->
@@ -1100,8 +1152,8 @@ ${ dashboard.layout_skeleton() }
             </div>
           </div>
         </div>
-
       </div>
+
       <div class="clearfix"></div>
 
       <!-- ko if: properties.scope() == 'stack' -->
@@ -1138,9 +1190,9 @@ ${ dashboard.layout_skeleton() }
     <div data-bind="with: $root.collection.getFacetById($parent.id())">
       <div data-bind="visible: $root.isEditing" style="margin-bottom: 20px">
         <span class="facet-field-label">${ _('Metric') }</span>
-        <select data-bind="options: HIT_OPTIONS, optionsText: 'label', optionsValue: 'value', value: properties.function"></select>
+        <select data-bind="options: HIT_OPTIONS, optionsText: 'label', optionsValue: 'value', value: properties.aggregate"></select>
       </div>
-      <div data-bind="visible: ! $root.isEditing(), text: getHitOption(properties.function())" class="muted"></div>
+      <div data-bind="visible: ! $root.isEditing(), text: getHitOption(properties.aggregate())" class="muted"></div>
     </div>
     <span class="big-counter" data-bind="textSqueezer: counts"></span>
   </div>
@@ -1536,6 +1588,7 @@ var viewModel;
 nv.dev = false;
 
 var HIT_OPTIONS = [
+  { value: "count", label: "${ _('Count') }" },
   { value: "unique", label: "${ _('Unique Count') }" },
   { value: "avg", label: "${ _('Average') }" },
   { value: "sum", label: "${ _('Sum') }" },
@@ -1546,7 +1599,7 @@ var HIT_OPTIONS = [
 ];
 
 function getHitOption(value){
-  for (var i=0;i<HIT_OPTIONS.length;i++){
+  for (var i=0; i < HIT_OPTIONS.length; i++){
     if (HIT_OPTIONS[i].value == value){
       return HIT_OPTIONS[i].label;
     }
@@ -1700,6 +1753,7 @@ function pivotChartDataTransformer(rawDatum) {
 
     var _key = Array.isArray(item.value) ? item.value[1] : item.value;
     var _category = null;
+
     _categories.forEach(function (category) {
       if (category.key == _key) {
         _category = category;

+ 13 - 16
apps/search/src/search/views.py

@@ -31,7 +31,7 @@ from libsolr.api import SolrApi
 from indexer.management.commands import indexer_setup
 
 from search.api import _guess_gap, _zoom_range_facet, _new_range_facet
-from search.conf import SOLR_URL
+from search.conf import SOLR_URL, LATEST
 from search.data_export import download as export_download
 from search.decorators import allow_owner_only, allow_viewer_only
 from search.management.commands import search_setup
@@ -60,7 +60,7 @@ def index(request):
   return render('search.mako', request, {
     'collection': collection,
     'query': query,
-    'initial': json.dumps({'collections': [], 'layout': []}),
+    'initial': json.dumps({'collections': [], 'layout': [], 'is_latest': LATEST.get()}),
     'is_owner': request.user == collection_doc.owner
   })
 
@@ -87,7 +87,8 @@ def new_search(request):
                   {"size":12,"name":"Grid Results","widgetType":"resultset-widget",
                    "properties":{},"offset":0,"isLoading":True,"klass":"card card-widget span12"}]}],
                  "drops":["temp"],"klass":"card card-home card-column span10"},
-         ]
+         ],
+         'is_latest': LATEST.get()
      }),
     'is_owner': True
   })
@@ -112,7 +113,8 @@ def browse(request, name):
                   {"size":12,"name":"Grid Results","id":"52f07188-f30f-1296-2450-f77e02e1a5c0","widgetType":"resultset-widget",
                    "properties":{},"offset":0,"isLoading":True,"klass":"card card-widget span12"}]}],
               "drops":["temp"],"klass":"card card-home card-column span10"}
-         ]
+         ],
+         'is_latest': LATEST.get()
      }),
      'is_owner': True
   })
@@ -480,7 +482,7 @@ def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_t
     'limit': 10,
     'mincount': 0,
     'isDate': False,
-    'function': 'unique' # new
+    'aggregate': 'unique'
   }
 
   if widget_type in ('tree-widget', 'heatmap-widget', 'map-widget'):
@@ -497,17 +499,12 @@ def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_t
       properties['initial_gap'] = properties['gap']
       properties['initial_start'] = properties['start']
       properties['initial_end'] = properties['end']
-      
-      facet_type = 'terms' # if 5.2+ --> unify all
-      properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 10, 'function': 'count'}
-      properties['facets'] = []
-      properties['scope'] = 'stack'
-      
     else:
-      #facet_type = 'field'
-      facet_type = 'terms' # if 5.2+
-      # New
-      properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 10, 'function': 'count'}
+      facet_type = 'field'
+
+    if widget_type == 'bucket-widget':
+      facet_type = 'nested'
+      properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 10, 'aggregate': 'count'}
       properties['facets'] = []
       properties['scope'] = 'stack'
 
@@ -515,7 +512,7 @@ def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_t
     properties['mincount'] = 1
     properties['facets'] = []
     properties['stacked'] = True
-    properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 5, 'function': 'count'} # todo
+    properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 5}
 
     if widget_type == 'map-widget':
       properties['scope'] = 'world'

+ 3 - 0
desktop/conf.dist/hue.ini

@@ -975,6 +975,9 @@
   ## Query sent when no term is entered
   ## empty_query=*:*
 
+  # Use latest Solr 5.2+ features.
+  ## latest=false
+
 
 ###########################################################################
 # Settings to configure Solr Indexer

+ 3 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -982,6 +982,9 @@
   ## Query sent when no term is entered
   ## empty_query=*:*
 
+  # Use latest Solr 5.2+ features.
+  ## latest=false
+
 
 ###########################################################################
 # Settings to configure Solr Indexer

+ 38 - 31
desktop/libs/libsolr/src/libsolr/api.py

@@ -65,6 +65,15 @@ class SolrApi(object):
     q_template = '(%s)' if len(query['qs']) >= 2 else '%s'
     return 'OR'.join([q_template % (q['q'] or EMPTY_QUERY.get()) for q in query['qs']]).encode('utf-8')
 
+  def _get_aggregate_function(self, facet):
+    props = {
+        'field': facet['field'],
+        'aggregate': facet['properties']['aggregate'] if 'properties' in facet else facet['aggregate']
+    }
+    if props['aggregate'] == 'median':
+      return 'percentile(%(field)s,50)' % props
+    else:
+      return '%(aggregate)s(%(field)s)' % props
 
   def _get_fq(self, query):
     params = ()
@@ -161,53 +170,52 @@ class SolrApi(object):
           params += (
               ('facet.field', '{!key=%(key)s ex=%(id)s f.%(field)s.facet.limit=%(limit)s f.%(field)s.facet.mincount=%(mincount)s}%(field)s' % keys),
           )
-        elif facet['type'] == 'terms':
+        elif facet['type'] == 'nested':
           props = {
               'key': '%(field)s-%(id)s' % facet
           }
           props.update(facet)
+
+          _f = {
+              'field': facet['field'],
+              'limit': int(facet['properties'].get('limit', 10)) + (1 if facet['widgetType'] == 'facet-widget' else 0),
+              'mincount': int(facet['properties']['mincount'])
+          }
+
           if 'start' in facet['properties']:
-            _f = {
+            _f.update({
                 'type': 'range',
-                'field': facet['field'],
                 'start': facet['properties']['start'],
                 'end': facet['properties']['end'],
                 'gap': facet['properties']['gap'],
-                'limit': int(facet['properties'].get('limit', 10)) + (1 if facet['widgetType'] == 'facet-widget' else 0),
-                'mincount': int(facet['properties']['mincount'])
-            }
-            if facet['properties']['facets']: # [{u'field': u'salary_d', u'functionz': u'avg', u'limit': 10, u'mincount': 1}
+            })
+          else:
+            _f.update({
+                'type': 'terms',
+                'field': facet['field'],
+            })
+
+          if facet['properties']['facets']:
+            if facet['properties']['facets'][0]['aggregate'] == 'count':
               _f['facet'] = {
                   'd2': {
                       'type': 'terms',
                       'field': '%(field)s' % facet['properties']['facets'][0]
-                  } 
+                  }
               }
-                        
-          else:
-            _f = {
-                'type': 'terms',
-                'field': facet['field'],
-                'limit': int(facet['properties'].get('limit', 10)) + (1 if facet['widgetType'] == 'facet-widget' else 0),
-                'mincount': int(facet['properties']['mincount'])
-            }
-            if facet['properties']['facets']: # [{u'field': u'salary_d', u'functionz': u'avg', u'limit': 10, u'mincount': 1}
+              if len(facet['properties']['facets']) > 1: # Get 3rd dimension calculation
+                _f['facet']['d2']['facet'] = {
+                    'd2': self._get_aggregate_function(facet['properties']['facets'][1])
+                }
+            else:
               _f['facet'] = {
-                  'd2': '%(functionz)s(%(field)s)' % facet['properties']['facets'][0] 
+                  'd2': self._get_aggregate_function(facet['properties']['facets'][0])
               }
-            
+
           json_facets['%(key)s' % props] = _f
         elif facet['type'] == 'function':
-          props = {
-              'function': facet['properties']['function'],
-              'key': '%(field)s-%(id)s' % facet
-          }
-          props.update(facet)
-          if facet['properties']['function'] == 'median':
-            props['formula'] = 'percentile(%(field)s,50)' % props
-          else:
-            props['formula'] = '%(function)s(%(field)s)' % props          
-          json_facets['%(key)s' % props] = '%(formula)s' % props
+          key = '%(field)s-%(id)s' % facet
+          json_facets[key] = self._get_aggregate_function(facet)
         elif facet['type'] == 'pivot':
           if facet['properties']['facets'] or facet['widgetType'] == 'map-widget':
             fields = facet['field']
@@ -233,10 +241,9 @@ class SolrApi(object):
         params += (
             ('json.facet', json.dumps(json_facets)),
         )
-        print json.dumps(json_facets)
 
     params += self._get_fq(query)
-    
+
     if collection['template']['fieldsSelected'] and collection['template']['isGridLayout']:
       fields = set(collection['template']['fieldsSelected'] + [collection['idField']] if collection['idField'] else [])
       # Add field if needed