<!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>