<!DOCTYPE html>
<html>

  <head>
    <script data-require="jquery@3.0.0" data-semver="3.0.0" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.js"></script>

    <script data-require="angular.js@1.5.6" data-semver="1.5.6" src="https://code.angularjs.org/1.5.6/angular.min.js"></script>
    <script data-require="sanitize@1.4.8" data-semver="1.4.8" src="https://code.angularjs.org/1.4.8/angular-sanitize.js"></script>

    <script data-require="ui-bootstrap@1.3.3" data-semver="1.3.3" src="https://cdn.rawgit.com/angular-ui/bootstrap/gh-pages/ui-bootstrap-tpls-1.3.3.js"></script>
    <link data-require="bootstrap@3.3.2" data-semver="3.3.2" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" />

    <script data-require="ui-select@0.18.1" data-semver="0.18.1" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-select/0.18.1/select.js"></script>
    <link data-require="ui-select@0.18.1" data-semver="0.18.1" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-select/0.18.1/select.css" />

    <script src="decision-tree.js"></script>
    <script src="script.js"></script>

    <link rel="stylesheet" href="style.css" />
  </head>

  <body ng-app="MainModule" ng-controller="MainController as ctrl">

    <!--<ui-select ng-model="ctrl.selectedDecisionTreeMode" theme="bootstrap" style="width: 250px;">-->
    <!--  <ui-select-match allow-clear="false" placeholder=""><span ng-bind="$select.selected.label"></span></ui-select-match>-->
    <!--  <ui-select-choices repeat="mode in (ctrl.decisionTreeModes | filter: { label: $select.search }) track by $index">-->
    <!--    <div ng-bind-html="mode.label | highlight: $select.search"></div>-->
    <!--  </ui-select-choices>-->
    <!--</ui-select>-->
    
    <p>This wizard walks helps you pick the right approach for performing a "GROUP BY" type operation 
                using elasticsearch</p>
                
    <p>Unlike regular searches, "group by" operations organise results under a choice of grouping key e.g. "productCode".</p>                
    <p>Distributed systems often make this kind of processing difficult which is why we offer a number of approaches, each with different trade-offs.</p>                
    
    
    <button class="btn btn-primary" ng-click="ctrl.openDecisionTreeWizard()">Launch Wizard</button>
    
    <decision-tree-result result="ctrl.result"></decision-tree-result>

  </body>

</html>
var mainModule = angular.module('MainModule', ['ui.select', 'ngSanitize', 'decision.tree']);

mainModule.controller('MainController', ['$decisionTree', function($decisionTree) {
  
  var vm = this;
  
  var RiskFactor = Object.freeze({ "HIGH": "HIGH", "MODERATE": "MODERATE", "LOW": "LOW"});
    
  var riskAssessmentTree =
    {
      question: "What results would you like to see under grouped keys?",
      children: [
        {
          value: "Summaries of many documents",
          question: "How do you want to sort the grouping keys?:",
          children: [
            { value: "By the key's value", result: { risk: RiskFactor.LOW, 
                description: 'Use the <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html">composite aggregation</a>',
                explanation:'An example might be a head office sales report, grouping sales by "Store" and then summing sales by products etc. If you have too many results to return in a single response, page through them use the <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html#_after">after</a> parameter'} },
            { value: "Some other sort order", 
                question: "How do you want to sort the grouping keys?:",
                children: [
                      { value: "Sum, avg, min, max, or count of a field's values", 
                            question: "How many unique keys do you need to return? ",
                            children: [
                              { value: "Few (less than 10,000)", 
                              
                                  question:"Accuracy may be a concern. How many unique keys are in your index?",
                                  "children":[
                                    { value:"Few (less than 100,000)", 
                                  result: { risk: RiskFactor.MODERATE, 
                                  description: 'Use the <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html">terms aggregation</a> - accuracy should be OK',
                                  explanation:'An example might be a top-products report, listing products ordered by the sum of their sales values.'+
                                  'Watch out for non-zero values in <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_calculating_document_count_error">doc_count_error_upper_bound</a> in results.'+
                                  ' If this happens consider increasing <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_shard_size_3">shard_size</a> '+
                                  'setting to trade RAM for accuracy.'} },
                                  {
                                    value:"Many (perhaps millions)",
                                    question:"What sort of client is making the request?",
                                    children:[
                                      {
                                        value:"A generic client (eg Kibana)",
                                        question: "How are you sorting keys?",
                                        children: [
                                          
                                          {
                                            "value":"largest of a value",
                                            result: { risk: RiskFactor.MODERATE, 
                                                description: 'Use the <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html">terms aggregation</a> - accuracy may be an issue',
                                  explanation:'An example might be a find high-value customers with the biggest spends.'+
                                  'Watch out for non-zero values in <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_calculating_document_count_error">doc_count_error_upper_bound</a> in results.'+
                                  ' If this happens consider increasing <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_shard_size_3">shard_size</a> '+
                                  'setting to trade RAM for accuracy. (Kibana cannot use multiple requests with terms partitioning to overcome accuracy issues).'
                                                
                                                     } 
                                          },
                                          {
                                            "value":"smallest of a value or cardinality count",
                                            result: { risk: RiskFactor.MODERATE, 
                                                      description: 'Use an <a target="_blank" href="https://twitter.com/elasticmark/status/1009380268409610240">entity-centric index</a> to overcome accuracy or memory constraints that are likely with these types of queries',
                                                      explanation: 'Example use case might be outlier detection - finding'+
                                                                   ' new process names by first-seen date, descending order.<br>'+
                                                                  'The disadvantage with this approach is that it requires maintenance of a secondary index. '+
                                                                  '<br>The advantage is that queries are fast and simple on pre-computed values.'
                                            } 
                                          }
                                        ]
                                      },
                                      {
                                        value:"Custom code calling the elasticsearch API)",
                                        
                                        
                                        question: "How are you sorting keys?",
                                        children: [
                                          
                                          {
                                            "value":"largest of a value",
                                            result: { risk: RiskFactor.MODERATE, 
                                                description: 'Use the <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html">terms aggregation</a> - accuracy may be an issue',
                                  explanation:'An example might be a find high-value customers with the biggest spends.'+
                                  'Watch out for non-zero values in <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_calculating_document_count_error">doc_count_error_upper_bound</a> in results.'+
                                  ' If this happens consider increasing <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_shard_size_3">shard_size</a> '+
                                  'setting to trade RAM for accuracy.'
                                                
                                                     } 
                                          },
                                          {
                                            "value":"smallest of a value or cardinality count",
                                            result: { risk: RiskFactor.MODERATE, 
                                                  description: 'Use the terms aggregation with <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_filtering_values_with_partitions">partitioning</a>'+
                                                  ' to deal with accuracy issues or memory constraints likely to be an issue with these types of requests.',
                                                  explanation:'An example might be a dormant-accounts report - find all users '+
                                                  'whose last logged activity was more than 2 years ago.<br>'+
                                                  'The disadvantage with this approach is that it requires careful sizing of partitions and '+
                                                  ' client-side stitching of results together.<br>The advantage is that each partition returned '+
                                                  ' can be made to have accurate results.'
                                                       } 
                                          }
                                        ]
                                      }
                                    ]
                                  }
                                  ]
                              },
                                { value: "Many (hundreds of thousands or more)", 
                                  result: { risk: RiskFactor.MODERATE, 
                                  description: 'Use the terms aggregation with <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#_filtering_values_with_partitions">partitioning</a>'+
                                  ' to deal with high data volumes',
                                  explanation:'An example might be a dormant-accounts report - find all users '+
                                  'whose last logged activity was more than 2 years ago.<br>'+
                                  'The disadvantage with this approach is that it requires careful sizing of partitions and '+
                                  ' client-side stitching of results together.<br>The advantage is that each partition returned '+
                                  ' can be made to have accurate results.'} }
          
                                                                
                           ]
                      },
                      { value: "Something more complex", 
                          result: { risk: RiskFactor.LOW, 
                                    description: 'Use an <a target="_blank" href="https://twitter.com/elasticmark/status/1009380268409610240">entity-centric index</a>',
                          explanation:'Often used for behavioural analysis. "Naughtiest car owners" are found by computing miles driven between'+
                          ' the date of a roadworthiness test failure report and the subsequent test pass<br>'+
                          'The disadvantage with this approach is that it requires maintenance of a secondary index. '+
                        '<br>The advantage is that queries are fast and simple on pre-computed values.'} }
                  
                  ]
            }
          ]
        },
        {
          value: "Individual raw documents",
          question: "How many keys do you need to page through?",
          children: [
            { value: "Few (maybe < one thousand)", result: { risk: RiskFactor.LOW, description: 'Use <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-collapse.html">field collapsing</a>',
              explanation:"Example use:  web searches that need to limit the documents returned from any one website"  } },
            
            
            {
              value: "Many (millions or more)",
              question: "How many concurrent users do you expect?",
              children: [
                  { value: "One", result: { risk: RiskFactor.MODERATE, description: 'Use the <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html">scroll API</a>',
                    explanation:"Typically used to export data in bulk" 
                  } },
                { value: "Many", result: { risk: RiskFactor.HIGH, description: "No solution",
                   explanation:"Your users will demand too much compute from the system. "+
                   "We can't let many users simultaneously perform deep pagination on these types of results."+
                   "Consider other ways to allow users to step through results e.g. offering filtering options"
                } }
              ]
            }            
            
          ]
        }
        
      ]
    };

	function tweak(tree, tweaker) {
	  tweaker(tree);
	  angular.forEach(tree.children, function (value) {
	    tweak(value, tweaker);
	    
	  });
	  
	  return tree;
	}

	vm.decisionTreeModes = [
		{ label: "ALL Buttons", tree: riskAssessmentTree },
		{ label: "ALL Selects", tree: tweak(
			angular.copy(riskAssessmentTree),
			function (child) {
				if (child.children) {
					child.selector = "drop-down";
				}
			}
		) },
		{ label: "Mixed (DDs when > 4 options)", tree: tweak(
			angular.copy(riskAssessmentTree),
			function (child) {
				if (child.children && child.children.length > 4) {
					child.selector = "drop-down";
				}
			}
		) }
	];
	vm.selectedDecisionTreeMode = vm.decisionTreeModes[0];

  vm.openDecisionTreeWizard = function () {
    
    $decisionTree.openWizard(
      {
        title: 'So you want to use elasticsearch to "Group by"...',
        resultTemplateUrl: 'risk-assessment-result.html',
        decisionTree: vm.selectedDecisionTreeMode.tree
      }  
    ).then(
      function (result) {
        vm.result = result;
      }
    );
  };

}]);

mainModule.directive('decisionTreeResult', function () {
  return {
    restrict: 'E',
    scope: {
      result: '='
    },
    templateUrl: 'risk-assessment-result.html'
  }
});
body {
  margin: 50px;
}
.explanation {
  font-style: italic;
}
button {
  margin: 5px;
}
<div ng-if="result">
    <label>Suggestion</label>
    <div class="alert" ng-class="{ 'alert-success': result.risk === 'LOW', 'alert-warning': result.risk === 'MODERATE', 'alert-danger': result.risk === 'HIGH'}" role="alert">
        <span ng-if="result.description" ng-bind-html="result.description"></span>        
    </div>
    <p class="explanation" ng-bind-html="result.explanation"></p>
</div>
angular.module('decision.tree', ['ui.select', 'ngSanitize', 'ui.bootstrap'])

.directive('decisionTree', ['$compile', '$timeout', function($compile, $timeout) {

    return {
        restrict: 'E',
        scope: {
            node: '=',
            onResult: '&',
            onClear: '&'
        },
        templateUrl: 'decision-tree.html',
        link: function(scope, element, attributes) {

            scope.decision = {};

            // Grab container for next decision
            var childDecisionContainer = element.find('.next-decision-container');

            // Shallow copy all the immediate children so we can safely set extra properties
            scope.children = [];
            angular.forEach(scope.node.children, function (child) {
                scope.children.push(angular.extend({}, child));
            });

            // Focus on the first "focusable" element found
            $timeout(function() {
                element.find('* [data-focusable]').first().focus();
                scope.$broadcast('new-decision-appended');
            });

            scope.selectNode = function(node) {

                scope.decision.selected = node;
            };

            scope.$watch('decision.selected', function (node) {

                if (node) {
                    angular.forEach(scope.children, function (child) {
                        child.selected = (child === node);
                    });

                    childDecisionContainer.empty();

                    if (node.result) {
                        if (scope.onResult) {
                            $timeout(function(){
                                scope.onResult({result: node.result});
                            });
                        }
                    } else {
                        if (scope.onClear) {
                            scope.onClear();
                        }

                        var el = angular.element('<decision-tree node="decision.selected" on-result="onResult({result: result})" on-clear="onClear()"></decision-tree>');
                        $compile(el)(scope);
                        childDecisionContainer.html(el);
                    }
                }
            });
        }
    };
}])

.directive('modalBody', ['$compile', '$templateRequest', function ($compile, $templateRequest) {

    return {
        restrict: 'C',
        link: function (scope, element, attributes) {

            var resultContainer = element.find('div.decision-tree-result');

            function injectResultTemplate(template) {

                var el = angular.element(template);

                $compile(el)(scope);
                resultContainer.html(el);
            }

            if (scope.resultTemplate) {
                injectResultTemplate(scope.resultTemplate);
            } else if (scope.resultTemplateUrl) {
                $templateRequest(scope.resultTemplateUrl)
                    .then(
                        function(template) {
                            injectResultTemplate(template);
                        }
                    );
            }
        }
    };
}])

.factory('$decisionTree', ['$uibModal', '$timeout', function ($uibModal, $timeout) {

    return {

        openWizard: function (config) {

            return $uibModal.open({
                animation: false,
                templateUrl: 'decision-tree-wizard-modal.html',
                resolve: {
                    config: function () { return config; }
                },
                controller: function ($scope, $uibModalInstance, config) {

                    $scope.title = config.title;
                    $scope.decisionTree = config.decisionTree;
                    $scope.resultTemplate = config.resultTemplate;
                    $scope.resultTemplateUrl = config.resultTemplateUrl;

                    $scope.onDecisionResult = function (result) {

                      $scope.result = result;
                        
                      $timeout(function() {
                            $('#acceptButton').focus();
                      });
                    };

                    $scope.onDecisionClear = function () {

                        $scope.result = undefined;
                    };
                }
            }).result;
        }
    };
}]);
<div class="decision-tree">
    <div class="container-row">
        <div class="form-group">
            <label>{{node.question ? node.question : 'Please select an option:'}}</label>

            <ng-switch on="node.selector">
                <div ng-switch-when="drop-down" class="decision-tree-select-wrapper">
                    <ui-select autofocus focus-on="new-decision-appended" ng-model="decision.selected" theme="bootstrap" style="width: 330px;">
                        <ui-select-match allow-clear="false" placeholder=""><span ng-bind="$select.selected.value"></span></ui-select-match>
                        <ui-select-choices repeat="child in (children | filter: { value: $select.search })">
                            <div ng-bind-html="child.value | highlight: $select.search"></div>
                        </ui-select-choices>
                    </ui-select>
                </div>
                <div ng-switch-default class="decision-tree-buttons-container">
                    <button autofocus data-focusable ng-repeat="child in children" ng-click="selectNode(child)" ng-bind="child.value" class="btn" ng-class="{'btn-success': child.selected, 'btn-default': !child.selected}"></button>
                </div>
            </ng-switch>
        </div>
    </div>
    <div class="next-decision-container"></div>
</div>
<div class="modal-header">
    <h3 class="modal-title" ng-bind="title"></h3>
</div>
<div class="modal-body">
    <decision-tree node="decisionTree" on-result="onDecisionResult(result)" on-clear="onDecisionClear()"></decision-tree>
    <div class="decision-tree-result"></div>
</div>
<div class="modal-footer">
    <button class="btn btn-primary" id="acceptButton" ng-click="$close(result)" ng-disabled="!result">Done</button>
    <button class="btn btn-default" ng-click="$dismiss()">Cancel</button>
</div>