mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[TSVB] Add Top Hits Aggregation (#17047)
* [TSVB] Adding Top Hits Aggregation * Fixing tests
This commit is contained in:
parent
0336858ed2
commit
3e5a220740
11 changed files with 404 additions and 163 deletions
|
@ -2,7 +2,6 @@ import { expect } from 'chai';
|
|||
import { createOptions, isBasicAgg } from '../agg_lookup';
|
||||
|
||||
describe('aggLookup', () => {
|
||||
|
||||
describe('isBasicAgg(metric)', () => {
|
||||
it('returns true for a basic metric (count)', () => {
|
||||
expect(isBasicAgg({ type: 'count' })).to.equal(true);
|
||||
|
@ -13,11 +12,10 @@ describe('aggLookup', () => {
|
|||
});
|
||||
|
||||
describe('createOptions(type, siblings)', () => {
|
||||
|
||||
it('returns options for all aggs', () => {
|
||||
const options = createOptions();
|
||||
expect(options).to.have.length(28);
|
||||
options.forEach((option) => {
|
||||
expect(options).to.have.length(29);
|
||||
options.forEach(option => {
|
||||
expect(option).to.have.property('label');
|
||||
expect(option).to.have.property('value');
|
||||
expect(option).to.have.property('disabled');
|
||||
|
@ -26,20 +24,23 @@ describe('aggLookup', () => {
|
|||
|
||||
it('returns options for basic', () => {
|
||||
const options = createOptions('basic');
|
||||
expect(options).to.have.length(14);
|
||||
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(true);
|
||||
expect(options).to.have.length(15);
|
||||
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns options for pipeline', () => {
|
||||
const options = createOptions('pipeline');
|
||||
expect(options).to.have.length(14);
|
||||
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(true);
|
||||
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns options for all if given unknown key', () => {
|
||||
const options = createOptions('foo');
|
||||
expect(options).to.have.length(28);
|
||||
expect(options).to.have.length(29);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
import _ from 'lodash';
|
||||
const lookup = {
|
||||
'count': 'Count',
|
||||
'calculation': 'Calculation',
|
||||
'std_deviation': 'Std. Deviation',
|
||||
'variance': 'Variance',
|
||||
'sum_of_squares': 'Sum of Sq.',
|
||||
'avg': 'Average',
|
||||
'max': 'Max',
|
||||
'min': 'Min',
|
||||
'sum': 'Sum',
|
||||
'percentile': 'Percentile',
|
||||
'percentile_rank': 'Percentile Rank',
|
||||
'cardinality': 'Cardinality',
|
||||
'value_count': 'Value Count',
|
||||
'derivative': 'Derivative',
|
||||
'cumulative_sum': 'Cumulative Sum',
|
||||
'moving_average': 'Moving Average',
|
||||
'avg_bucket': 'Overall Average',
|
||||
'min_bucket': 'Overall Min',
|
||||
'max_bucket': 'Overall Max',
|
||||
'sum_bucket': 'Overall Sum',
|
||||
'variance_bucket': 'Overall Variance',
|
||||
'sum_of_squares_bucket': 'Overall Sum of Sq.',
|
||||
'std_deviation_bucket': 'Overall Std. Deviation',
|
||||
'series_agg': 'Series Agg',
|
||||
'serial_diff': 'Serial Difference',
|
||||
'filter_ratio': 'Filter Ratio',
|
||||
'positive_only': 'Positive Only',
|
||||
'static': 'Static Value'
|
||||
count: 'Count',
|
||||
calculation: 'Calculation',
|
||||
std_deviation: 'Std. Deviation',
|
||||
variance: 'Variance',
|
||||
sum_of_squares: 'Sum of Sq.',
|
||||
avg: 'Average',
|
||||
max: 'Max',
|
||||
min: 'Min',
|
||||
sum: 'Sum',
|
||||
percentile: 'Percentile',
|
||||
percentile_rank: 'Percentile Rank',
|
||||
cardinality: 'Cardinality',
|
||||
value_count: 'Value Count',
|
||||
derivative: 'Derivative',
|
||||
cumulative_sum: 'Cumulative Sum',
|
||||
moving_average: 'Moving Average',
|
||||
avg_bucket: 'Overall Average',
|
||||
min_bucket: 'Overall Min',
|
||||
max_bucket: 'Overall Max',
|
||||
sum_bucket: 'Overall Sum',
|
||||
variance_bucket: 'Overall Variance',
|
||||
sum_of_squares_bucket: 'Overall Sum of Sq.',
|
||||
std_deviation_bucket: 'Overall Std. Deviation',
|
||||
series_agg: 'Series Agg',
|
||||
serial_diff: 'Serial Difference',
|
||||
filter_ratio: 'Filter Ratio',
|
||||
positive_only: 'Positive Only',
|
||||
static: 'Static Value',
|
||||
top_hit: 'Top Hit',
|
||||
};
|
||||
|
||||
const pipeline = [
|
||||
|
@ -44,7 +45,7 @@ const pipeline = [
|
|||
'std_deviation_bucket',
|
||||
'series_agg',
|
||||
'serial_diff',
|
||||
'positive_only'
|
||||
'positive_only',
|
||||
];
|
||||
|
||||
const byType = {
|
||||
|
@ -58,8 +59,8 @@ const byType = {
|
|||
'max',
|
||||
'sum',
|
||||
'cardinality',
|
||||
'value_count'
|
||||
])
|
||||
'value_count',
|
||||
]),
|
||||
};
|
||||
|
||||
export function isBasicAgg(item) {
|
||||
|
@ -68,16 +69,18 @@ export function isBasicAgg(item) {
|
|||
|
||||
export function createOptions(type = '_all', siblings = []) {
|
||||
let aggs = byType[type];
|
||||
if (!aggs) aggs = byType._all;
|
||||
if (!aggs) aggs = byType._all;
|
||||
let enablePipelines = siblings.some(isBasicAgg);
|
||||
if (siblings.length <= 1) enablePipelines = false;
|
||||
return _(aggs)
|
||||
.map((label, value) => {
|
||||
const disabled = _.includes(pipeline, value) ? !enablePipelines : false;
|
||||
return {
|
||||
label: disabled ? `${label} (use the "+" button to add this pipeline agg)` : label,
|
||||
label: disabled
|
||||
? `${label} (use the "+" button to add this pipeline agg)`
|
||||
: label,
|
||||
value,
|
||||
disabled
|
||||
disabled,
|
||||
};
|
||||
})
|
||||
.value();
|
||||
|
|
|
@ -12,7 +12,7 @@ const paths = [
|
|||
'variance_bucket',
|
||||
'sum_of_squares_bucket',
|
||||
'serial_diff',
|
||||
'positive_only'
|
||||
'positive_only',
|
||||
];
|
||||
export default function calculateLabel(metric, metrics) {
|
||||
if (!metric) return 'Unknown';
|
||||
|
@ -28,7 +28,6 @@ export default function calculateLabel(metric, metrics) {
|
|||
return `${lookup[metric.type]} (${metric.value}) of ${metric.field}`;
|
||||
}
|
||||
|
||||
|
||||
if (includes(paths, metric.type)) {
|
||||
let additionalLabel = '';
|
||||
const targetMetric = metrics.find(m => startsWith(metric.field, m.id));
|
||||
|
@ -48,4 +47,3 @@ export default function calculateLabel(metric, metrics) {
|
|||
|
||||
return `${lookup[metric.type]} of ${metric.field}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@ const metricAggs = [
|
|||
{ label: 'Std. Deviation', value: 'std_deviation' },
|
||||
{ label: 'Sum', value: 'sum' },
|
||||
{ label: 'Sum of Squares', value: 'sum_of_squares' },
|
||||
{ label: 'Top Hit', value: 'top_hit' },
|
||||
{ label: 'Value Count', value: 'value_count' },
|
||||
{ label: 'Variance', value: 'variance' }
|
||||
{ label: 'Variance', value: 'variance' },
|
||||
];
|
||||
|
||||
const pipelineAggs = [
|
||||
|
@ -26,7 +27,7 @@ const pipelineAggs = [
|
|||
{ label: 'Moving Average', value: 'moving_average' },
|
||||
{ label: 'Positive Only', value: 'positive_only' },
|
||||
{ label: 'Serial Difference', value: 'serial_diff' },
|
||||
{ label: 'Series Agg', value: 'series_agg' }
|
||||
{ label: 'Series Agg', value: 'series_agg' },
|
||||
];
|
||||
|
||||
const siblingAggs = [
|
||||
|
@ -36,11 +37,10 @@ const siblingAggs = [
|
|||
{ label: 'Overall Std. Deviation', value: 'std_deviation_bucket' },
|
||||
{ label: 'Overall Sum', value: 'sum_bucket' },
|
||||
{ label: 'Overall Sum of Squares', value: 'sum_of_squares_bucket' },
|
||||
{ label: 'Overall Variance', value: 'variance_bucket' }
|
||||
{ label: 'Overall Variance', value: 'variance_bucket' },
|
||||
];
|
||||
|
||||
class AggSelectOption extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
|
@ -66,7 +66,7 @@ class AggSelectOption extends Component {
|
|||
render() {
|
||||
const { label, heading, pipeline } = this.props.option;
|
||||
const style = {
|
||||
paddingLeft: heading ? 0 : 10
|
||||
paddingLeft: heading ? 0 : 10,
|
||||
};
|
||||
// We can ignore that the <div> does not have keyboard handlers even though
|
||||
// it has mouse handlers, since react-select still takes care, that this works
|
||||
|
@ -75,7 +75,11 @@ class AggSelectOption extends Component {
|
|||
if (heading) {
|
||||
let note;
|
||||
if (pipeline) {
|
||||
note = (<span className="vis_editor__agg_select-note">(requires child aggregation)</span>);
|
||||
note = (
|
||||
<span className="vis_editor__agg_select-note">
|
||||
(requires child aggregation)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
@ -101,13 +105,12 @@ class AggSelectOption extends Component {
|
|||
aria-label={label}
|
||||
>
|
||||
<span className="Select-value-label" style={style}>
|
||||
{ this.props.children }
|
||||
{this.props.children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AggSelectOption.props = {
|
||||
|
@ -131,7 +134,9 @@ function filterByPanelType(panelType) {
|
|||
function AggSelect(props) {
|
||||
const { siblings, panelType } = props;
|
||||
|
||||
let enablePipelines = siblings.some(s => !!metricAggs.find(m => m.value === s.type));
|
||||
let enablePipelines = siblings.some(
|
||||
s => !!metricAggs.find(m => m.value === s.type)
|
||||
);
|
||||
if (siblings.length <= 1) enablePipelines = false;
|
||||
|
||||
let options;
|
||||
|
@ -139,16 +144,35 @@ function AggSelect(props) {
|
|||
options = metricAggs;
|
||||
} else {
|
||||
options = [
|
||||
{ label: 'Metric Aggregations', value: null, heading: true, disabled: true },
|
||||
{
|
||||
label: 'Metric Aggregations',
|
||||
value: null,
|
||||
heading: true,
|
||||
disabled: true,
|
||||
},
|
||||
...metricAggs,
|
||||
{ label: 'Parent Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true },
|
||||
...pipelineAggs.filter(filterByPanelType(panelType)).map(agg => ({ ...agg, disabled: !enablePipelines })),
|
||||
{ label: 'Sibling Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true },
|
||||
...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines }))
|
||||
{
|
||||
label: 'Parent Pipeline Aggregations',
|
||||
value: null,
|
||||
pipeline: true,
|
||||
heading: true,
|
||||
disabled: true,
|
||||
},
|
||||
...pipelineAggs
|
||||
.filter(filterByPanelType(panelType))
|
||||
.map(agg => ({ ...agg, disabled: !enablePipelines })),
|
||||
{
|
||||
label: 'Sibling Pipeline Aggregations',
|
||||
value: null,
|
||||
pipeline: true,
|
||||
heading: true,
|
||||
disabled: true,
|
||||
},
|
||||
...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
|
||||
];
|
||||
}
|
||||
|
||||
const handleChange = (value) => {
|
||||
const handleChange = value => {
|
||||
if (!value) return;
|
||||
if (value.disabled) return;
|
||||
if (value.value) props.onChange(value);
|
||||
|
@ -172,7 +196,7 @@ AggSelect.propTypes = {
|
|||
onChange: PropTypes.func,
|
||||
panelType: PropTypes.string,
|
||||
siblings: PropTypes.array,
|
||||
value: PropTypes.string
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AggSelect;
|
||||
|
|
|
@ -7,7 +7,7 @@ import calculateLabel from '../../../common/calculate_label';
|
|||
import basicAggs from '../../../common/basic_aggs';
|
||||
|
||||
function createTypeFilter(restrict, exclude) {
|
||||
return (metric) => {
|
||||
return metric => {
|
||||
if (_.includes(exclude, metric.type)) return false;
|
||||
switch (restrict) {
|
||||
case 'basic':
|
||||
|
@ -18,25 +18,20 @@ function createTypeFilter(restrict, exclude) {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
// This filters out sibling aggs, percentiles, and special aggs (like Series Agg)
|
||||
export function filterRows(row) {
|
||||
return !/_bucket$/.test(row.type)
|
||||
&& !/^series/.test(row.type)
|
||||
&& !/^percentile/.test(row.type);
|
||||
return (
|
||||
!/_bucket$/.test(row.type) &&
|
||||
!/^series/.test(row.type) &&
|
||||
!/^percentile/.test(row.type) &&
|
||||
!/^top_hit/.test(row.type)
|
||||
);
|
||||
}
|
||||
|
||||
function MetricSelect(props) {
|
||||
const {
|
||||
restrict,
|
||||
metric,
|
||||
onChange,
|
||||
value,
|
||||
exclude
|
||||
} = props;
|
||||
const { restrict, metric, onChange, value, exclude } = props;
|
||||
|
||||
const metrics = props.metrics
|
||||
.filter(createTypeFilter(restrict, exclude));
|
||||
const metrics = props.metrics.filter(createTypeFilter(restrict, exclude));
|
||||
|
||||
const siblings = calculateSiblings(metrics, metric);
|
||||
|
||||
|
@ -50,25 +45,25 @@ function MetricSelect(props) {
|
|||
row.percentiles.forEach(p => {
|
||||
if (p.value) {
|
||||
const value = /\./.test(p.value) ? p.value : `${p.value}.0`;
|
||||
acc.push({ value: `${row.id}[${value}]`, label: `${label} (${value})` });
|
||||
acc.push({
|
||||
value: `${row.id}[${value}]`,
|
||||
label: `${label} (${value})`,
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
|
||||
const options = siblings
|
||||
.filter(filterRows)
|
||||
.map(row => {
|
||||
const label = calculateLabel(row, metrics);
|
||||
return { value: row.id, label };
|
||||
});
|
||||
const options = siblings.filter(filterRows).map(row => {
|
||||
const label = calculateLabel(row, metrics);
|
||||
return { value: row.id, label };
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label="Select metric"
|
||||
placeholder="Select metric..."
|
||||
options={[ ...options, ...props.additionalOptions, ...percentileOptions]}
|
||||
options={[...options, ...props.additionalOptions, ...percentileOptions]}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
@ -88,7 +83,7 @@ MetricSelect.propTypes = {
|
|||
metric: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
restrict: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
export default MetricSelect;
|
||||
|
|
129
src/core_plugins/metrics/public/components/aggs/top_hit.js
Normal file
129
src/core_plugins/metrics/public/components/aggs/top_hit.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
import React from 'react';
|
||||
import AggRow from './agg_row';
|
||||
import AggSelect from './agg_select';
|
||||
import FieldSelect from './field_select';
|
||||
import Select from 'react-select';
|
||||
import createChangeHandler from '../lib/create_change_handler';
|
||||
import createSelectHandler from '../lib/create_select_handler';
|
||||
import createTextHandler from '../lib/create_text_handler';
|
||||
import { htmlIdGenerator } from '@elastic/eui';
|
||||
|
||||
export const TopHitAgg = props => {
|
||||
const { fields, series, panel } = props;
|
||||
const defaults = {
|
||||
agg_with: 'avg',
|
||||
size: 1,
|
||||
order: 'desc',
|
||||
};
|
||||
const model = { ...defaults, ...props.model };
|
||||
const indexPattern =
|
||||
(series.override_index_pattern && series.series_index_pattern) ||
|
||||
panel.index_pattern;
|
||||
|
||||
const handleChange = createChangeHandler(props.onChange, model);
|
||||
const handleSelectChange = createSelectHandler(handleChange);
|
||||
const handleTextChange = createTextHandler(handleChange);
|
||||
|
||||
const aggWithOptions = [
|
||||
{ label: 'Avg', value: 'avg' },
|
||||
{ label: 'Max', value: 'max' },
|
||||
{ label: 'Min', value: 'min' },
|
||||
{ label: 'Sum', value: 'sum' },
|
||||
];
|
||||
|
||||
const orderOptions = [
|
||||
{ label: 'Asc', value: 'asc' },
|
||||
{ label: 'Desc', value: 'desc' },
|
||||
];
|
||||
|
||||
const htmlId = htmlIdGenerator();
|
||||
return (
|
||||
<AggRow
|
||||
disableDelete={props.disableDelete}
|
||||
model={props.model}
|
||||
onAdd={props.onAdd}
|
||||
onDelete={props.onDelete}
|
||||
siblings={props.siblings}
|
||||
>
|
||||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__agg_row-item">
|
||||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
/>
|
||||
</div>
|
||||
<div className="vis_editor__row_item">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('field')}>
|
||||
Field
|
||||
</label>
|
||||
<FieldSelect
|
||||
id={htmlId('field')}
|
||||
fields={fields}
|
||||
type={model.type}
|
||||
restrict="numeric"
|
||||
indexPattern={indexPattern}
|
||||
value={model.field}
|
||||
onChange={handleSelectChange('field')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vis_editor__agg_row-item">
|
||||
<div className="vis_editor__row_item">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('size')}>
|
||||
Size
|
||||
</label>
|
||||
<input
|
||||
id={htmlId('size')}
|
||||
className="vis_editor__input-grows-100"
|
||||
value={model.size}
|
||||
onChange={handleTextChange('size')}
|
||||
/>
|
||||
</div>
|
||||
<div className="vis_editor__row_item">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('agg_with')}>
|
||||
Aggregate with
|
||||
</label>
|
||||
<Select
|
||||
inputProps={{ id: htmlId('agg_with') }}
|
||||
clearable={false}
|
||||
placeholder="Select..."
|
||||
onChange={handleSelectChange('agg_with')}
|
||||
value={model.agg_with}
|
||||
options={aggWithOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="vis_editor__row_item">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('order_by')}>
|
||||
Order by
|
||||
</label>
|
||||
<FieldSelect
|
||||
id={htmlId('order_by')}
|
||||
restrict="date"
|
||||
value={model.order_by}
|
||||
onChange={handleSelectChange('order_by')}
|
||||
indexPattern={indexPattern}
|
||||
fields={fields}
|
||||
/>
|
||||
</div>
|
||||
<div className="vis_editor__row_item">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('order')}>
|
||||
Order
|
||||
</label>
|
||||
<Select
|
||||
inputProps={{ id: htmlId('order') }}
|
||||
clearable={false}
|
||||
placeholder="Select..."
|
||||
onChange={handleSelectChange('order')}
|
||||
value={model.order}
|
||||
options={orderOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AggRow>
|
||||
);
|
||||
};
|
|
@ -12,6 +12,7 @@ import { PositiveOnlyAgg } from '../aggs/positive_only';
|
|||
import { FilterRatioAgg } from '../aggs/filter_ratio';
|
||||
import { PercentileRankAgg } from '../aggs/percentile_rank';
|
||||
import { Static } from '../aggs/static';
|
||||
import { TopHitAgg } from '../aggs/top_hit';
|
||||
export default {
|
||||
count: StdAgg,
|
||||
avg: StdAgg,
|
||||
|
@ -40,7 +41,6 @@ export default {
|
|||
serial_diff: SerialDiffAgg,
|
||||
filter_ratio: FilterRatioAgg,
|
||||
positive_only: PositiveOnlyAgg,
|
||||
static: Static
|
||||
static: Static,
|
||||
top_hit: TopHitAgg,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -5,75 +5,122 @@ function testAgg(row, metric, expected) {
|
|||
let name = metric.type;
|
||||
if (metric.mode) name += `(${metric.mode})`;
|
||||
if (metric.percent) name += `(${metric.percent})`;
|
||||
it(`it should return ${name}`, () => {
|
||||
it(`it should return ${name}(${expected})`, () => {
|
||||
const value = getAggValue(row, metric);
|
||||
expect(value).to.eql(expected);
|
||||
});
|
||||
}
|
||||
|
||||
describe('getAggValue', () => {
|
||||
|
||||
describe('extended_stats', () => {
|
||||
const row = {
|
||||
'test': {
|
||||
'count': 9,
|
||||
'min': 72,
|
||||
'max': 99,
|
||||
'avg': 86,
|
||||
'sum': 774,
|
||||
'sum_of_squares': 67028,
|
||||
'variance': 51.55555555555556,
|
||||
'std_deviation': 7.180219742846005,
|
||||
'std_deviation_bounds': {
|
||||
'upper': 100.36043948569201,
|
||||
'lower': 71.63956051430799
|
||||
}
|
||||
}
|
||||
test: {
|
||||
count: 9,
|
||||
min: 72,
|
||||
max: 99,
|
||||
avg: 86,
|
||||
sum: 774,
|
||||
sum_of_squares: 67028,
|
||||
variance: 51.55555555555556,
|
||||
std_deviation: 7.180219742846005,
|
||||
std_deviation_bounds: {
|
||||
upper: 100.36043948569201,
|
||||
lower: 71.63956051430799,
|
||||
},
|
||||
},
|
||||
};
|
||||
testAgg(row, { id: 'test', type: 'std_deviation' }, 7.180219742846005);
|
||||
testAgg(row, { id: 'test', type: 'variance' }, 51.55555555555556);
|
||||
testAgg(row, { id: 'test', type: 'sum_of_squares' }, 67028);
|
||||
testAgg(row, { id: 'test', type: 'std_deviation', mode: 'upper' }, 100.36043948569201);
|
||||
testAgg(row, { id: 'test', type: 'std_deviation', mode: 'lower' }, 71.63956051430799);
|
||||
testAgg(
|
||||
row,
|
||||
{ id: 'test', type: 'std_deviation', mode: 'upper' },
|
||||
100.36043948569201
|
||||
);
|
||||
testAgg(
|
||||
row,
|
||||
{ id: 'test', type: 'std_deviation', mode: 'lower' },
|
||||
71.63956051430799
|
||||
);
|
||||
});
|
||||
|
||||
describe('percentile', () => {
|
||||
const row = {
|
||||
'test': {
|
||||
'values': {
|
||||
test: {
|
||||
values: {
|
||||
'1.0': 15,
|
||||
'5.0': 20,
|
||||
'25.0': 23,
|
||||
'50.0': 25,
|
||||
'75.0': 29,
|
||||
'95.0': 60,
|
||||
'99.0': 150
|
||||
}
|
||||
}
|
||||
'99.0': 150,
|
||||
},
|
||||
},
|
||||
};
|
||||
testAgg(row, { id: 'test', type: 'percentile', percent: '50' }, 25);
|
||||
testAgg(row, { id: 'test', type: 'percentile', percent: '1.0' }, 15);
|
||||
});
|
||||
|
||||
describe('top hits', () => {
|
||||
const row = {
|
||||
test: {
|
||||
doc_count: 1,
|
||||
docs: {
|
||||
hits: {
|
||||
hits: [
|
||||
{ _source: { example: { value: 25 } } },
|
||||
{ _source: { example: { value: 25 } } },
|
||||
{ _source: { example: { value: 25 } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
testAgg(
|
||||
row,
|
||||
{ id: 'test', type: 'top_hit', agg_with: 'avg', field: 'example.value' },
|
||||
25
|
||||
);
|
||||
testAgg(
|
||||
row,
|
||||
{ id: 'test', type: 'top_hit', agg_with: 'sum', field: 'example.value' },
|
||||
75
|
||||
);
|
||||
testAgg(
|
||||
row,
|
||||
{ id: 'test', type: 'top_hit', agg_with: 'max', field: 'example.value' },
|
||||
25
|
||||
);
|
||||
testAgg(
|
||||
row,
|
||||
{ id: 'test', type: 'top_hit', agg_with: 'min', field: 'example.value' },
|
||||
25
|
||||
);
|
||||
});
|
||||
|
||||
const basicWithDerv = {
|
||||
'key_as_string': '2015/02/01 00:00:00',
|
||||
'key': 1422748800000,
|
||||
'doc_count': 2,
|
||||
'test': {
|
||||
'value': 60.0
|
||||
key_as_string: '2015/02/01 00:00:00',
|
||||
key: 1422748800000,
|
||||
doc_count: 2,
|
||||
test: {
|
||||
value: 60.0,
|
||||
},
|
||||
test_deriv: {
|
||||
value: -490.0,
|
||||
normalized_value: -15.806451612903226,
|
||||
},
|
||||
'test_deriv': {
|
||||
'value': -490.0,
|
||||
'normalized_value': -15.806451612903226
|
||||
}
|
||||
};
|
||||
|
||||
describe('derivative', () => {
|
||||
testAgg(basicWithDerv, { id: 'test_deriv', type: 'derivative' }, -15.806451612903226);
|
||||
testAgg(
|
||||
basicWithDerv,
|
||||
{ id: 'test_deriv', type: 'derivative' },
|
||||
-15.806451612903226
|
||||
);
|
||||
});
|
||||
|
||||
describe('basic metric', () => {
|
||||
testAgg(basicWithDerv, { id: 'test', type: 'avg' }, 60.0);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import parseSettings from './parse_settings';
|
||||
import getBucketsPath from './get_buckets_path';
|
||||
import { parseInterval } from './parse_interval';
|
||||
import { set } from 'lodash';
|
||||
|
||||
function checkMetric(metric, fields) {
|
||||
fields.forEach(field => {
|
||||
|
@ -14,7 +15,7 @@ function stdMetric(bucket) {
|
|||
checkMetric(bucket, ['type', 'field']);
|
||||
const body = {};
|
||||
body[bucket.type] = {
|
||||
field: bucket.field
|
||||
field: bucket.field,
|
||||
};
|
||||
return body;
|
||||
}
|
||||
|
@ -22,7 +23,7 @@ function stdMetric(bucket) {
|
|||
function extendStats(bucket) {
|
||||
checkMetric(bucket, ['type', 'field']);
|
||||
const body = {
|
||||
extended_stats: { field: bucket.field }
|
||||
extended_stats: { field: bucket.field },
|
||||
};
|
||||
if (bucket.sigma) body.extended_stats.sigma = parseInt(bucket.sigma, 10);
|
||||
return body;
|
||||
|
@ -44,10 +45,10 @@ export default {
|
|||
buckets_path: { count: '_count' },
|
||||
script: {
|
||||
source: 'count * 1',
|
||||
lang: 'expression'
|
||||
lang: 'expression',
|
||||
},
|
||||
gap_policy: 'skip'
|
||||
}
|
||||
gap_policy: 'skip',
|
||||
},
|
||||
};
|
||||
},
|
||||
static: bucket => {
|
||||
|
@ -57,10 +58,10 @@ export default {
|
|||
buckets_path: { count: '_count' },
|
||||
script: {
|
||||
source: bucket.value,
|
||||
lang: 'painless'
|
||||
lang: 'painless',
|
||||
},
|
||||
gap_policy: 'skip'
|
||||
}
|
||||
gap_policy: 'skip',
|
||||
},
|
||||
};
|
||||
},
|
||||
avg: stdMetric,
|
||||
|
@ -73,13 +74,36 @@ export default {
|
|||
variance: extendStats,
|
||||
std_deviation: extendStats,
|
||||
|
||||
top_hit: bucket => {
|
||||
checkMetric(bucket, ['type', 'field', 'size']);
|
||||
const body = {
|
||||
filter: {
|
||||
exists: { field: bucket.field },
|
||||
},
|
||||
aggs: {
|
||||
docs: {
|
||||
top_hits: {
|
||||
size: bucket.size,
|
||||
_source: { includes: [bucket.field] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (bucket.order_by) {
|
||||
set(body, 'aggs.docs.top_hits.sort', [
|
||||
{ [bucket.order_by]: { order: bucket.order } },
|
||||
]);
|
||||
}
|
||||
return body;
|
||||
},
|
||||
|
||||
percentile_rank: bucket => {
|
||||
checkMetric(bucket, ['type', 'field', 'value']);
|
||||
const body = {
|
||||
percentile_ranks: {
|
||||
field: bucket.field,
|
||||
values: [bucket.value]
|
||||
}
|
||||
values: [bucket.value],
|
||||
},
|
||||
};
|
||||
return body;
|
||||
},
|
||||
|
@ -105,8 +129,8 @@ export default {
|
|||
const agg = {
|
||||
percentiles: {
|
||||
field: bucket.field,
|
||||
percents
|
||||
}
|
||||
percents,
|
||||
},
|
||||
};
|
||||
return agg;
|
||||
},
|
||||
|
@ -117,8 +141,8 @@ export default {
|
|||
derivative: {
|
||||
buckets_path: getBucketsPath(bucket.field, metrics),
|
||||
gap_policy: 'skip', // seems sane
|
||||
unit: bucketSize
|
||||
}
|
||||
unit: bucketSize,
|
||||
},
|
||||
};
|
||||
if (bucket.gap_policy) body.derivative.gap_policy = bucket.gap_policy;
|
||||
if (bucket.unit) {
|
||||
|
@ -135,8 +159,8 @@ export default {
|
|||
serial_diff: {
|
||||
buckets_path: getBucketsPath(bucket.field, metrics),
|
||||
gap_policy: 'skip', // seems sane
|
||||
lag: 1
|
||||
}
|
||||
lag: 1,
|
||||
},
|
||||
};
|
||||
if (bucket.gap_policy) body.serial_diff.gap_policy = bucket.gap_policy;
|
||||
if (bucket.lag) {
|
||||
|
@ -149,8 +173,8 @@ export default {
|
|||
checkMetric(bucket, ['type', 'field']);
|
||||
return {
|
||||
cumulative_sum: {
|
||||
buckets_path: getBucketsPath(bucket.field, metrics)
|
||||
}
|
||||
buckets_path: getBucketsPath(bucket.field, metrics),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -160,8 +184,8 @@ export default {
|
|||
moving_avg: {
|
||||
buckets_path: getBucketsPath(bucket.field, metrics),
|
||||
model: bucket.model || 'simple',
|
||||
gap_policy: 'skip' // seems sane
|
||||
}
|
||||
gap_policy: 'skip', // seems sane
|
||||
},
|
||||
};
|
||||
if (bucket.gap_policy) body.moving_avg.gap_policy = bucket.gap_policy;
|
||||
if (bucket.window) body.moving_avg.window = Number(bucket.window);
|
||||
|
@ -185,11 +209,11 @@ export default {
|
|||
source: bucket.script,
|
||||
lang: 'painless',
|
||||
params: {
|
||||
_interval: parseInterval(bucketSize).asMilliseconds()
|
||||
}
|
||||
_interval: parseInterval(bucketSize).asMilliseconds(),
|
||||
},
|
||||
},
|
||||
gap_policy: 'skip' // seems sane
|
||||
}
|
||||
gap_policy: 'skip', // seems sane
|
||||
},
|
||||
};
|
||||
if (bucket.gap_policy) body.bucket_script.gap_policy = bucket.gap_policy;
|
||||
return body;
|
||||
|
@ -200,15 +224,15 @@ export default {
|
|||
const body = {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
value: getBucketsPath(bucket.field, metrics)
|
||||
value: getBucketsPath(bucket.field, metrics),
|
||||
},
|
||||
script: {
|
||||
source: 'params.value > 0 ? params.value : 0',
|
||||
lang: 'painless'
|
||||
lang: 'painless',
|
||||
},
|
||||
gap_policy: 'skip' // seems sane
|
||||
}
|
||||
gap_policy: 'skip', // seems sane
|
||||
},
|
||||
};
|
||||
return body;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import _ from 'lodash';
|
||||
import { get, includes, max, min, sum } from 'lodash';
|
||||
import extendStatsTypes from './extended_stats_types';
|
||||
|
||||
const aggFns = {
|
||||
max,
|
||||
min,
|
||||
sum,
|
||||
avg: values => sum(values) / values.length,
|
||||
};
|
||||
|
||||
export default (row, metric) => {
|
||||
// Extended Stats
|
||||
if (_.includes(extendStatsTypes, metric.type)) {
|
||||
if (includes(extendStatsTypes, metric.type)) {
|
||||
const isStdDeviation = /^std_deviation/.test(metric.type);
|
||||
const modeIsBounds = ~['upper', 'lower'].indexOf(metric.mode);
|
||||
if (isStdDeviation && modeIsBounds) {
|
||||
return _.get(row, `${metric.id}.std_deviation_bounds.${metric.mode}`);
|
||||
return get(row, `${metric.id}.std_deviation_bounds.${metric.mode}`);
|
||||
}
|
||||
return _.get(row, `${metric.id}.${metric.type}`);
|
||||
return get(row, `${metric.id}.${metric.type}`);
|
||||
}
|
||||
|
||||
// Percentiles
|
||||
|
@ -22,14 +30,27 @@ export default (row, metric) => {
|
|||
|
||||
if (metric.type === 'percentile_rank') {
|
||||
const percentileRankKey = `${metric.value}`;
|
||||
return row[metric.id] && row[metric.id].values && row[metric.id].values[percentileRankKey];
|
||||
return (
|
||||
row[metric.id] &&
|
||||
row[metric.id].values &&
|
||||
row[metric.id].values[percentileRankKey]
|
||||
);
|
||||
}
|
||||
|
||||
if (metric.type === 'top_hit') {
|
||||
if (row[metric.id].doc_count === 0) return null;
|
||||
const hits = get(row, [metric.id, 'docs', 'hits', 'hits'], []);
|
||||
const values = hits.map(doc => {
|
||||
return get(doc, `_source.${metric.field}`, 0);
|
||||
});
|
||||
const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.avg;
|
||||
return aggWith(values);
|
||||
}
|
||||
|
||||
// Derivatives
|
||||
const normalizedValue = _.get(row, `${metric.id}.normalized_value`, null);
|
||||
const normalizedValue = get(row, `${metric.id}.normalized_value`, null);
|
||||
|
||||
// Everything else
|
||||
const value = _.get(row, `${metric.id}.value`, null);
|
||||
const value = get(row, `${metric.id}.value`, null);
|
||||
return normalizedValue || value;
|
||||
|
||||
};
|
||||
|
|
|
@ -13,17 +13,16 @@ export default function stdMetric(resp, panel, series) {
|
|||
}
|
||||
if (/_bucket$/.test(metric.type)) return next(results);
|
||||
const decoration = getDefaultDecoration(series);
|
||||
getSplits(resp, panel, series).forEach((split) => {
|
||||
getSplits(resp, panel, series).forEach(split => {
|
||||
const data = split.timeseries.buckets.map(mapBucket(metric));
|
||||
results.push({
|
||||
id: `${split.id}`,
|
||||
label: split.label,
|
||||
color: split.color,
|
||||
data,
|
||||
...decoration
|
||||
...decoration,
|
||||
});
|
||||
});
|
||||
return next(results);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue