Multi field terms (#116928)

This commit is contained in:
Joe Reuter 2021-11-23 17:25:37 +01:00 committed by GitHub
parent 7bd903bd16
commit 079db9666e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1133 additions and 225 deletions

View file

@ -56,6 +56,7 @@ export const getAggTypes = () => ({
{ name: BUCKET_TYPES.DATE_RANGE, fn: buckets.getDateRangeBucketAgg },
{ name: BUCKET_TYPES.IP_RANGE, fn: buckets.getIpRangeBucketAgg },
{ name: BUCKET_TYPES.TERMS, fn: buckets.getTermsBucketAgg },
{ name: BUCKET_TYPES.MULTI_TERMS, fn: buckets.getMultiTermsBucketAgg },
{ name: BUCKET_TYPES.FILTER, fn: buckets.getFilterBucketAgg },
{ name: BUCKET_TYPES.FILTERS, fn: buckets.getFiltersBucketAgg },
{ name: BUCKET_TYPES.SIGNIFICANT_TERMS, fn: buckets.getSignificantTermsBucketAgg },
@ -77,6 +78,7 @@ export const getAggTypesFunctions = () => [
buckets.aggHistogram,
buckets.aggDateHistogram,
buckets.aggTerms,
buckets.aggMultiTerms,
metrics.aggAvg,
metrics.aggBucketAvg,
metrics.aggBucketMax,

View file

@ -67,6 +67,7 @@ describe('Aggs service', () => {
"date_range",
"ip_range",
"terms",
"multi_terms",
"filter",
"filters",
"significant_terms",
@ -115,6 +116,7 @@ describe('Aggs service', () => {
"date_range",
"ip_range",
"terms",
"multi_terms",
"filter",
"filters",
"significant_terms",

View file

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import moment from 'moment';
import { IBucketAggConfig, BucketAggParam } from './bucket_agg_type';
export const termsAggFilter = [
'!top_hits',
'!percentiles',
'!std_dev',
'!derivative',
'!moving_avg',
'!serial_diff',
'!cumulative_sum',
'!avg_bucket',
'!max_bucket',
'!min_bucket',
'!sum_bucket',
];
export const termsOrderAggParamDefinition: Partial<BucketAggParam<IBucketAggConfig>> = {
name: 'orderAgg',
type: 'agg',
allowedAggs: termsAggFilter,
default: null,
makeAgg(termsAgg, state = { type: 'count' }) {
state.schema = 'orderAgg';
const orderAgg = termsAgg.aggConfigs.createAggConfig<IBucketAggConfig>(state, {
addToAggConfigs: false,
});
orderAgg.id = termsAgg.id + '-orderAgg';
return orderAgg;
},
write(agg, output, aggs) {
const dir = agg.params.order.value;
const order: Record<string, any> = (output.params.order = {});
let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy);
// TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings
// thus causing issues with filtering. This probably causes other issues since float might not
// be able to contain the number on the elasticsearch side
if (output.params.script) {
output.params.value_type = agg.getField().type === 'number' ? 'float' : agg.getField().type;
}
if (agg.params.missingBucket && agg.params.field.type === 'string') {
output.params.missing = '__missing__';
}
if (!orderAgg) {
order[agg.params.orderBy || '_count'] = dir;
return;
}
if (aggs?.hasTimeShifts() && Object.keys(aggs?.getTimeShifts()).length > 1 && aggs.timeRange) {
const shift = orderAgg.getTimeShift();
orderAgg = aggs.createAggConfig(
{
type: 'filtered_metric',
id: orderAgg.id,
params: {
customBucket: aggs
.createAggConfig(
{
type: 'filter',
id: 'shift',
params: {
filter: {
language: 'lucene',
query: {
range: {
[aggs.timeFields![0]]: {
gte: moment(aggs.timeRange.from)
.subtract(shift || 0)
.toISOString(),
lte: moment(aggs.timeRange.to)
.subtract(shift || 0)
.toISOString(),
},
},
},
},
},
},
{
addToAggConfigs: false,
}
)
.serialize(),
customMetric: orderAgg.serialize(),
},
enabled: false,
},
{
addToAggConfigs: false,
}
);
}
if (orderAgg.type.name === 'count') {
order._count = dir;
return;
}
const orderAggPath = orderAgg.getValueBucketPath();
if (orderAgg.parentId && aggs) {
orderAgg = aggs.byId(orderAgg.parentId);
}
output.subAggs = (output.subAggs || []).concat(orderAgg);
order[orderAggPath] = dir;
},
};

View file

@ -11,6 +11,7 @@ import {
mergeOtherBucketAggResponse,
updateMissingBucket,
OTHER_BUCKET_SEPARATOR as SEP,
constructSingleTermOtherFilter,
} from './_terms_other_bucket_helper';
import { AggConfigs, CreateAggConfigParams } from '../agg_configs';
import { BUCKET_TYPES } from './bucket_agg_types';
@ -573,7 +574,8 @@ describe('Terms Agg Other bucket helper', () => {
singleTermResponse,
singleOtherResponse,
aggConfigs.aggs[0] as IBucketAggConfig,
otherAggConfig()
otherAggConfig(),
constructSingleTermOtherFilter
);
expect((mergedResponse!.aggregations!['1'] as any).buckets[3].key).toEqual('__other__');
}
@ -594,7 +596,8 @@ describe('Terms Agg Other bucket helper', () => {
nestedTermResponse,
nestedOtherResponse,
aggConfigs.aggs[1] as IBucketAggConfig,
otherAggConfig()
otherAggConfig(),
constructSingleTermOtherFilter
);
expect((mergedResponse!.aggregations!['1'] as any).buckets[1]['2'].buckets[3].key).toEqual(

View file

@ -7,11 +7,18 @@
*/
import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash';
import { i18n } from '@kbn/i18n';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { buildExistsFilter, buildPhrasesFilter, buildQueryFromFilters } from '@kbn/es-query';
import {
buildExistsFilter,
buildPhrasesFilter,
buildQueryFromFilters,
Filter,
} from '@kbn/es-query';
import { AggGroupNames } from '../agg_groups';
import { IAggConfigs } from '../agg_configs';
import { IBucketAggConfig } from './bucket_agg_type';
import { IAggType } from '../agg_type';
import { IAggConfig } from '../agg_config';
export const OTHER_BUCKET_SEPARATOR = '╰┄►';
@ -44,7 +51,7 @@ const getNestedAggDSL = (aggNestedDsl: Record<string, any>, startFromAggId: stri
const getAggResultBuckets = (
aggConfigs: IAggConfigs,
response: estypes.SearchResponse<any>['aggregations'],
aggWithOtherBucket: IBucketAggConfig,
aggWithOtherBucket: IAggConfig,
key: string
) => {
const keyParts = key.split(OTHER_BUCKET_SEPARATOR);
@ -111,11 +118,7 @@ const getAggConfigResultMissingBuckets = (responseAggs: any, aggId: string) => {
* @param key: the key for this specific other bucket
* @param otherAgg: AggConfig of the aggregation with other bucket
*/
const getOtherAggTerms = (
requestAgg: Record<string, any>,
key: string,
otherAgg: IBucketAggConfig
) => {
const getOtherAggTerms = (requestAgg: Record<string, any>, key: string, otherAgg: IAggConfig) => {
return requestAgg['other-filter'].filters.filters[key].bool.must_not
.filter(
(filter: Record<string, any>) =>
@ -126,7 +129,7 @@ const getOtherAggTerms = (
export const buildOtherBucketAgg = (
aggConfigs: IAggConfigs,
aggWithOtherBucket: IBucketAggConfig,
aggWithOtherBucket: IAggConfig,
response: any
) => {
const bucketAggs = aggConfigs.aggs.filter(
@ -200,12 +203,16 @@ export const buildOtherBucketAgg = (
return;
}
const hasScriptedField = !!aggWithOtherBucket.params.field.scripted;
const hasScriptedField = !!aggWithOtherBucket.params.field?.scripted;
const hasMissingBucket = !!aggWithOtherBucket.params.missingBucket;
const hasMissingBucketKey = agg.buckets.some(
(bucket: { key: string }) => bucket.key === '__missing__'
);
if (!hasScriptedField && (!hasMissingBucket || hasMissingBucketKey)) {
if (
aggWithOtherBucket.params.field &&
!hasScriptedField &&
(!hasMissingBucket || hasMissingBucketKey)
) {
filters.push(
buildExistsFilter(
aggWithOtherBucket.params.field,
@ -217,7 +224,7 @@ export const buildOtherBucketAgg = (
// create not filters for all the buckets
each(agg.buckets, (bucket) => {
if (bucket.key === '__missing__') return;
const filter = currentAgg.createFilter(bucket.key);
const filter = currentAgg.createFilter(currentAgg.getKey(bucket, bucket.key));
filter.meta.negate = true;
filters.push(filter);
});
@ -244,8 +251,9 @@ export const mergeOtherBucketAggResponse = (
aggsConfig: IAggConfigs,
response: estypes.SearchResponse<any>,
otherResponse: any,
otherAgg: IBucketAggConfig,
requestAgg: Record<string, any>
otherAgg: IAggConfig,
requestAgg: Record<string, any>,
otherFilterBuilder: (requestAgg: Record<string, any>, key: string, otherAgg: IAggConfig) => Filter
): estypes.SearchResponse<any> => {
const updatedResponse = cloneDeep(response);
each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => {
@ -257,15 +265,8 @@ export const mergeOtherBucketAggResponse = (
otherAgg,
bucketKey
);
const requestFilterTerms = getOtherAggTerms(requestAgg, key, otherAgg);
const phraseFilter = buildPhrasesFilter(
otherAgg.params.field,
requestFilterTerms,
otherAgg.aggConfigs.indexPattern
);
phraseFilter.meta.negate = true;
bucket.filters = [phraseFilter];
const otherFilter = otherFilterBuilder(requestAgg, key, otherAgg);
bucket.filters = [otherFilter];
bucket.key = '__other__';
if (
@ -285,7 +286,7 @@ export const mergeOtherBucketAggResponse = (
export const updateMissingBucket = (
response: estypes.SearchResponse<any>,
aggConfigs: IAggConfigs,
agg: IBucketAggConfig
agg: IAggConfig
) => {
const updatedResponse = cloneDeep(response);
const aggResultBuckets = getAggConfigResultMissingBuckets(updatedResponse.aggregations, agg.id);
@ -294,3 +295,84 @@ export const updateMissingBucket = (
});
return updatedResponse;
};
export function constructSingleTermOtherFilter(
requestAgg: Record<string, any>,
key: string,
otherAgg: IAggConfig
) {
const requestFilterTerms = getOtherAggTerms(requestAgg, key, otherAgg);
const phraseFilter = buildPhrasesFilter(
otherAgg.params.field,
requestFilterTerms,
otherAgg.aggConfigs.indexPattern
);
phraseFilter.meta.negate = true;
return phraseFilter;
}
export function constructMultiTermOtherFilter(
requestAgg: Record<string, any>,
key: string
): Filter {
return {
query: requestAgg['other-filter'].filters.filters[key],
meta: {},
};
}
export const createOtherBucketPostFlightRequest = (
otherFilterBuilder: (requestAgg: Record<string, any>, key: string, otherAgg: IAggConfig) => Filter
) => {
const postFlightRequest: IAggType['postFlightRequest'] = async (
resp,
aggConfigs,
aggConfig,
searchSource,
inspectorRequestAdapter,
abortSignal,
searchSessionId
) => {
if (!resp.aggregations) return resp;
const nestedSearchSource = searchSource.createChild();
if (aggConfig.params.otherBucket) {
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
if (!filterAgg) return resp;
nestedSearchSource.setField('aggs', filterAgg);
const { rawResponse: response } = await nestedSearchSource
.fetch$({
abortSignal,
sessionId: searchSessionId,
inspector: {
adapter: inspectorRequestAdapter,
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
defaultMessage: 'Other bucket',
}),
description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
defaultMessage:
'This request counts the number of documents that fall ' +
'outside the criterion of the data buckets.',
}),
},
})
.toPromise();
resp = mergeOtherBucketAggResponse(
aggConfigs,
resp,
response,
aggConfig,
filterAgg(),
otherFilterBuilder
);
}
if (aggConfig.params.missingBucket) {
resp = updateMissingBucket(resp, aggConfigs, aggConfig);
}
return resp;
};
return postFlightRequest;
};

View file

@ -14,6 +14,7 @@ export enum BUCKET_TYPES {
DATE_RANGE = 'date_range',
RANGE = 'range',
TERMS = 'terms',
MULTI_TERMS = 'multi_terms',
SIGNIFICANT_TERMS = 'significant_terms',
GEOHASH_GRID = 'geohash_grid',
GEOTILE_GRID = 'geotile_grid',

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { buildPhraseFilter, Filter } from '@kbn/es-query';
import { IBucketAggConfig } from '../bucket_agg_type';
import { MultiFieldKey } from '../multi_field_key';
export const createFilterMultiTerms = (
aggConfig: IBucketAggConfig,
key: MultiFieldKey,
params: { terms: MultiFieldKey[] }
): Filter => {
const fields = aggConfig.params.fields;
const indexPattern = aggConfig.aggConfigs.indexPattern;
if (String(key) === '__other__') {
const multiTerms = params.terms;
const perMultiTermQuery = multiTerms.map((multiTerm) =>
multiTerm.keys.map(
(partialKey, i) =>
buildPhraseFilter(indexPattern.getFieldByName(fields[i])!, partialKey, indexPattern).query
)
);
return {
meta: {
negate: true,
alias: multiTerms
.map((multiTerm) => multiTerm.keys.join(', '))
.join(
` ${i18n.translate('data.search.aggs.buckets.multiTerms.otherFilterJoinName', {
defaultMessage: 'or',
})} `
),
index: indexPattern.id,
},
query: {
bool: {
should: perMultiTermQuery.map((multiTermQuery) => ({
bool: {
must: multiTermQuery,
},
})),
minimum_should_match: 1,
},
},
};
}
const partials = key.keys.map((partialKey, i) =>
buildPhraseFilter(indexPattern.getFieldByName(fields[i])!, partialKey, indexPattern)
);
return {
meta: {
alias: key.keys.join(', '),
index: indexPattern.id,
},
query: {
bool: {
must: partials.map((partialFilter) => partialFilter.query),
},
},
};
};

View file

@ -36,3 +36,5 @@ export * from './significant_terms_fn';
export * from './significant_terms';
export * from './terms_fn';
export * from './terms';
export * from './multi_terms_fn';
export * from './multi_terms';

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const id = Symbol('id');
const isBucketLike = (bucket: unknown): bucket is { key: unknown } => {
return Boolean(bucket && typeof bucket === 'object' && 'key' in bucket);
};
function getKeysFromBucket(bucket: unknown) {
if (!isBucketLike(bucket)) {
throw new Error('bucket malformed - no key found');
}
return Array.isArray(bucket.key)
? bucket.key.map((keyPart) => String(keyPart))
: [String(bucket.key)];
}
export class MultiFieldKey {
[id]: string;
keys: string[];
constructor(bucket: unknown) {
this.keys = getKeysFromBucket(bucket);
this[id] = MultiFieldKey.idBucket(bucket);
}
static idBucket(bucket: unknown) {
return getKeysFromBucket(bucket).join(',');
}
toString() {
return this[id];
}
}

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AggConfigs } from '../agg_configs';
import { METRIC_TYPES } from '../metrics';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
import type { IndexPatternField } from '../../..';
import { IndexPattern } from '../../..';
describe('Multi Terms Agg', () => {
const getAggConfigs = (params: Record<string, any> = {}) => {
const indexPattern = {
id: '1234',
title: 'logstash-*',
fields: [
{
name: 'field',
type: 'string',
esTypes: ['string'],
aggregatable: true,
filterable: true,
searchable: true,
},
{
name: 'string_field',
type: 'string',
esTypes: ['string'],
aggregatable: true,
filterable: true,
searchable: true,
},
{
name: 'empty_number_field',
type: 'number',
esTypes: ['number'],
aggregatable: true,
filterable: true,
searchable: true,
},
{
name: 'number_field',
type: 'number',
esTypes: ['number'],
aggregatable: true,
filterable: true,
searchable: true,
},
],
} as IndexPattern;
indexPattern.fields.getByName = (name) => ({ name } as unknown as IndexPatternField);
indexPattern.fields.filter = () => indexPattern.fields;
return new AggConfigs(
indexPattern,
[
{
id: 'test',
params,
type: BUCKET_TYPES.MULTI_TERMS,
},
],
{ typesRegistry: mockAggTypesRegistry() }
);
};
test('produces the expected expression ast', () => {
const aggConfigs = getAggConfigs({
fields: ['field', 'string_field'],
orderAgg: {
type: 'count',
},
});
expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(`
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"fields": Array [
"field",
"string_field",
],
"id": Array [
"test",
],
"order": Array [
"desc",
],
"orderAgg": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"test-orderAgg",
],
"schema": Array [
"orderAgg",
],
},
"function": "aggCount",
"type": "function",
},
],
"type": "expression",
},
],
"otherBucket": Array [
false,
],
"otherBucketLabel": Array [
"Other",
],
"size": Array [
5,
],
},
"function": "aggTerms",
"type": "function",
},
],
"type": "expression",
}
`);
});
test('uses correct bucket path for sorting by median', () => {
const indexPattern = {
id: '1234',
title: 'logstash-*',
fields: [
{
name: 'string_field',
type: 'string',
esTypes: ['string'],
aggregatable: true,
filterable: true,
searchable: true,
},
{
name: 'number_field',
type: 'number',
esTypes: ['number'],
aggregatable: true,
filterable: true,
searchable: true,
},
],
} as IndexPattern;
indexPattern.fields.getByName = (name) => ({ name } as unknown as IndexPatternField);
indexPattern.fields.filter = () => indexPattern.fields;
const aggConfigs = new AggConfigs(
indexPattern,
[
{
id: 'test',
params: {
fields: ['string_field'],
orderAgg: {
type: METRIC_TYPES.MEDIAN,
params: {
field: {
name: 'number_field',
type: 'number',
},
},
},
},
type: BUCKET_TYPES.MULTI_TERMS,
},
],
{ typesRegistry: mockAggTypesRegistry() }
);
const { [BUCKET_TYPES.MULTI_TERMS]: params } = aggConfigs.aggs[0].toDsl();
expect(params.order).toEqual({ 'test-orderAgg.50': 'desc' });
});
});

View file

@ -0,0 +1,147 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { noop } from 'lodash';
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { createFilterMultiTerms } from './create_filter/multi_terms';
import { aggTermsFnName } from './terms_fn';
import { AggConfigSerialized, BaseAggParams } from '../types';
import { MultiFieldKey } from './multi_field_key';
import {
createOtherBucketPostFlightRequest,
constructMultiTermOtherFilter,
} from './_terms_other_bucket_helper';
import { termsOrderAggParamDefinition } from './_terms_order_helper';
const termsTitle = i18n.translate('data.search.aggs.buckets.multiTermsTitle', {
defaultMessage: 'Multi-Terms',
});
export interface AggParamsMultiTerms extends BaseAggParams {
fields: string[];
orderBy: string;
orderAgg?: AggConfigSerialized;
order?: 'asc' | 'desc';
size?: number;
otherBucket?: boolean;
otherBucketLabel?: string;
}
export const getMultiTermsBucketAgg = () => {
const keyCaches = new WeakMap();
return new BucketAggType({
name: BUCKET_TYPES.MULTI_TERMS,
expressionName: aggTermsFnName,
title: termsTitle,
makeLabel(agg) {
const params = agg.params;
return agg.getFieldDisplayName() + ': ' + params.order.text;
},
getKey(bucket, key, agg) {
let keys = keyCaches.get(agg);
if (!keys) {
keys = new Map();
keyCaches.set(agg, keys);
}
const id = MultiFieldKey.idBucket(bucket);
key = keys.get(id);
if (!key) {
key = new MultiFieldKey(bucket);
keys.set(id, key);
}
return key;
},
getSerializedFormat(agg) {
const params = agg.params as AggParamsMultiTerms;
const formats = params.fields
? params.fields.map((field) => {
const fieldSpec = agg.aggConfigs.indexPattern.getFieldByName(field);
if (!fieldSpec) {
return {
id: undefined,
params: undefined,
};
}
return agg.aggConfigs.indexPattern.getFormatterForField(fieldSpec).toJSON();
})
: [{ id: undefined, params: undefined }];
return {
id: 'multi_terms',
params: {
otherBucketLabel: params.otherBucketLabel,
paramsPerField: formats,
},
};
},
createFilter: createFilterMultiTerms,
postFlightRequest: createOtherBucketPostFlightRequest(constructMultiTermOtherFilter),
params: [
{
name: 'fields',
write(agg, output, aggs) {
const params = agg.params as AggParamsMultiTerms;
output.params.terms = params.fields.map((field) => ({ field }));
},
},
{
name: 'orderBy',
write: noop, // prevent default write, it's handled by orderAgg
},
termsOrderAggParamDefinition,
{
name: 'order',
type: 'optioned',
default: 'desc',
options: [
{
text: i18n.translate('data.search.aggs.buckets.terms.orderDescendingTitle', {
defaultMessage: 'Descending',
}),
value: 'desc',
},
{
text: i18n.translate('data.search.aggs.buckets.terms.orderAscendingTitle', {
defaultMessage: 'Ascending',
}),
value: 'asc',
},
],
write: noop, // prevent default write, it's handled by orderAgg
},
{
name: 'size',
default: 5,
},
{
name: 'otherBucket',
default: false,
write: noop,
},
{
name: 'otherBucketLabel',
type: 'string',
default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', {
defaultMessage: 'Other',
}),
displayName: i18n.translate('data.search.aggs.otherBucket.labelForOtherBucketLabel', {
defaultMessage: 'Label for other bucket',
}),
shouldShow: (agg) => agg.getParam('otherBucket'),
write: noop,
},
],
});
};

View file

@ -0,0 +1,132 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
export const aggMultiTermsFnName = 'aggMultiTerms';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.MULTI_TERMS>;
// Since the orderAgg param is an agg nested in a subexpression, we need to
// overwrite the param type to expect a value of type AggExpressionType.
type Arguments = Assign<AggArgs, { orderAgg?: AggExpressionType }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggMultiTermsFnName,
Input,
Arguments,
Output
>;
export const aggMultiTerms = (): FunctionDefinition => ({
name: aggMultiTermsFnName,
help: i18n.translate('data.search.aggs.function.buckets.multiTerms.help', {
defaultMessage: 'Generates a serialized agg config for a Multi-Terms agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.buckets.multiTerms.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
fields: {
types: ['string'],
multi: true,
required: true,
help: i18n.translate('data.search.aggs.buckets.multiTerms.fields.help', {
defaultMessage: 'Fields to use for this aggregation',
}),
},
order: {
types: ['string'],
options: ['asc', 'desc'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.order.help', {
defaultMessage: 'Order in which to return the results: asc or desc',
}),
},
orderBy: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.orderBy.help', {
defaultMessage: 'Field to order results by',
}),
},
orderAgg: {
types: ['agg_type'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.orderAgg.help', {
defaultMessage: 'Agg config to use for ordering results',
}),
},
size: {
types: ['number'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.size.help', {
defaultMessage: 'Max number of buckets to retrieve',
}),
},
otherBucket: {
types: ['boolean'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.otherBucket.help', {
defaultMessage: 'When set to true, groups together any buckets beyond the allowed size',
}),
},
otherBucketLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.otherBucketLabel.help', {
defaultMessage: 'Default label used in charts for documents in the Other bucket',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.json.help', {
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.multiTerms.customLabel.help', {
defaultMessage: 'Represents a custom label for this aggregation',
}),
},
},
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
return {
type: 'agg_type',
value: {
id,
enabled,
schema,
type: BUCKET_TYPES.MULTI_TERMS,
params: {
...rest,
orderAgg: args.orderAgg?.value,
},
},
};
},
});

View file

@ -9,8 +9,7 @@
import { noop } from 'lodash';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { createFilterTerms } from './create_filter/terms';
import {
@ -23,24 +22,12 @@ import { AggConfigSerialized, BaseAggParams } from '../types';
import { KBN_FIELD_TYPES } from '../../../../common';
import {
buildOtherBucketAgg,
mergeOtherBucketAggResponse,
updateMissingBucket,
createOtherBucketPostFlightRequest,
constructSingleTermOtherFilter,
} from './_terms_other_bucket_helper';
import { termsOrderAggParamDefinition } from './_terms_order_helper';
export const termsAggFilter = [
'!top_hits',
'!percentiles',
'!std_dev',
'!derivative',
'!moving_avg',
'!serial_diff',
'!cumulative_sum',
'!avg_bucket',
'!max_bucket',
'!min_bucket',
'!sum_bucket',
];
export { termsAggFilter } from './_terms_order_helper';
const termsTitle = i18n.translate('data.search.aggs.buckets.termsTitle', {
defaultMessage: 'Terms',
@ -85,49 +72,8 @@ export const getTermsBucketAgg = () =>
};
},
createFilter: createFilterTerms,
postFlightRequest: createOtherBucketPostFlightRequest(constructSingleTermOtherFilter),
hasPrecisionError: (aggBucket) => Boolean(aggBucket?.doc_count_error_upper_bound),
postFlightRequest: async (
resp,
aggConfigs,
aggConfig,
searchSource,
inspectorRequestAdapter,
abortSignal,
searchSessionId
) => {
if (!resp.aggregations) return resp;
const nestedSearchSource = searchSource.createChild();
if (aggConfig.params.otherBucket) {
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
if (!filterAgg) return resp;
nestedSearchSource.setField('aggs', filterAgg);
const { rawResponse: response } = await nestedSearchSource
.fetch$({
abortSignal,
sessionId: searchSessionId,
inspector: {
adapter: inspectorRequestAdapter,
title: i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
defaultMessage: 'Other bucket',
}),
description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
defaultMessage:
'This request counts the number of documents that fall ' +
'outside the criterion of the data buckets.',
}),
},
})
.toPromise();
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
}
if (aggConfig.params.missingBucket) {
resp = updateMissingBucket(resp, aggConfigs, aggConfig);
}
return resp;
},
params: [
{
name: 'field',
@ -144,106 +90,7 @@ export const getTermsBucketAgg = () =>
name: 'orderBy',
write: noop, // prevent default write, it's handled by orderAgg
},
{
name: 'orderAgg',
type: 'agg',
allowedAggs: termsAggFilter,
default: null,
makeAgg(termsAgg, state = { type: 'count' }) {
state.schema = 'orderAgg';
const orderAgg = termsAgg.aggConfigs.createAggConfig<IBucketAggConfig>(state, {
addToAggConfigs: false,
});
orderAgg.id = termsAgg.id + '-orderAgg';
return orderAgg;
},
write(agg, output, aggs) {
const dir = agg.params.order.value;
const order: Record<string, any> = (output.params.order = {});
let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy);
// TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings
// thus causing issues with filtering. This probably causes other issues since float might not
// be able to contain the number on the elasticsearch side
if (output.params.script) {
output.params.value_type =
agg.getField().type === 'number' ? 'float' : agg.getField().type;
}
if (agg.params.missingBucket && agg.params.field.type === 'string') {
output.params.missing = '__missing__';
}
if (!orderAgg) {
order[agg.params.orderBy || '_count'] = dir;
return;
}
if (
aggs?.hasTimeShifts() &&
Object.keys(aggs?.getTimeShifts()).length > 1 &&
aggs.timeRange
) {
const shift = orderAgg.getTimeShift();
orderAgg = aggs.createAggConfig(
{
type: 'filtered_metric',
id: orderAgg.id,
params: {
customBucket: aggs
.createAggConfig(
{
type: 'filter',
id: 'shift',
params: {
filter: {
language: 'lucene',
query: {
range: {
[aggs.timeFields![0]]: {
gte: moment(aggs.timeRange.from)
.subtract(shift || 0)
.toISOString(),
lte: moment(aggs.timeRange.to)
.subtract(shift || 0)
.toISOString(),
},
},
},
},
},
},
{
addToAggConfigs: false,
}
)
.serialize(),
customMetric: orderAgg.serialize(),
},
enabled: false,
},
{
addToAggConfigs: false,
}
);
}
if (orderAgg.type.name === 'count') {
order._count = dir;
return;
}
const orderAggPath = orderAgg.getValueBucketPath();
if (orderAgg.parentId && aggs) {
orderAgg = aggs.byId(orderAgg.parentId);
}
output.subAggs = (output.subAggs || []).concat(orderAgg);
order[orderAggPath] = dir;
},
},
termsOrderAggParamDefinition,
{
name: 'order',
type: 'optioned',

View file

@ -67,6 +67,7 @@ import {
AggParamsStdDeviation,
AggParamsSum,
AggParamsTerms,
AggParamsMultiTerms,
AggParamsTopHit,
aggPercentileRanks,
aggPercentiles,
@ -76,6 +77,7 @@ import {
aggStdDeviation,
aggSum,
aggTerms,
aggMultiTerms,
aggTopHit,
AggTypesRegistry,
AggTypesRegistrySetup,
@ -163,6 +165,7 @@ export interface AggParamsMapping {
[BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram;
[BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram;
[BUCKET_TYPES.TERMS]: AggParamsTerms;
[BUCKET_TYPES.MULTI_TERMS]: AggParamsMultiTerms;
[METRIC_TYPES.AVG]: AggParamsAvg;
[METRIC_TYPES.CARDINALITY]: AggParamsCardinality;
[METRIC_TYPES.COUNT]: BaseAggParams;
@ -203,6 +206,7 @@ export interface AggFunctionsMapping {
aggHistogram: ReturnType<typeof aggHistogram>;
aggDateHistogram: ReturnType<typeof aggDateHistogram>;
aggTerms: ReturnType<typeof aggTerms>;
aggMultiTerms: ReturnType<typeof aggMultiTerms>;
aggAvg: ReturnType<typeof aggAvg>;
aggBucketAvg: ReturnType<typeof aggBucketAvg>;
aggBucketMax: ReturnType<typeof aggBucketMax>;

View file

@ -19,6 +19,7 @@ import {
import { DateRange } from '../../expressions';
import { convertDateRangeToString } from '../buckets/lib/date_range';
import { convertIPRangeToString, IpRangeKey } from '../buckets/lib/ip_range';
import { MultiFieldKey } from '../buckets/multi_field_key';
type GetFieldFormat = (mapping: SerializedFieldFormat) => IFieldFormat;
@ -128,5 +129,25 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta
};
getConverterFor = (type: FieldFormatsContentType) => (val: string) => this.convert(val, type);
},
class AggsMultiTermsFieldFormat extends FieldFormat {
static id = 'multi_terms';
static hidden = true;
convert = (val: unknown, type: FieldFormatsContentType) => {
const params = this._params;
const formats = (params.paramsPerField as SerializedFieldFormat[]).map((fieldParams) =>
getFieldFormat({ id: fieldParams.id, params: fieldParams })
);
if (String(val) === '__other__') {
return params.otherBucketLabel;
}
return (val as MultiFieldKey).keys
.map((valPart, i) => formats[i].convert(valPart, type))
.join(' ');
};
getConverterFor = (type: FieldFormatsContentType) => (val: string) => this.convert(val, type);
},
];
}

View file

@ -53,8 +53,8 @@ const getOtherBucketFilterTerms = (
return [
...new Set(
terms.filter((term) => {
const notOther = term !== '__other__';
const notMissing = term !== '__missing__';
const notOther = String(term) !== '__other__';
const notMissing = String(term) !== '__missing__';
return notOther && notMissing;
})
),
@ -99,7 +99,10 @@ const createFilter = async (
if (value === null || value === undefined || !aggConfig.isFilterable()) {
return;
}
if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) {
if (
(aggConfig.type.name === 'terms' || aggConfig.type.name === 'multi_terms') &&
aggConfig.params.otherBucket
) {
const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex);
filter = aggConfig.createFilter(value, { terms });
} else {

View file

@ -53,7 +53,7 @@ describe('AggsService - public', () => {
test('registers default agg types', () => {
service.setup(setupDeps);
const start = service.start(startDeps);
expect(start.types.getAll().buckets.length).toBe(11);
expect(start.types.getAll().buckets.length).toBe(12);
expect(start.types.getAll().metrics.length).toBe(23);
});
@ -69,7 +69,7 @@ describe('AggsService - public', () => {
);
const start = service.start(startDeps);
expect(start.types.getAll().buckets.length).toBe(12);
expect(start.types.getAll().buckets.length).toBe(13);
expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true);
expect(start.types.getAll().metrics.length).toBe(24);
expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true);

View file

@ -86,7 +86,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition<VisParams> =>
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -87,7 +87,7 @@ export const samplePieVis = {
title: 'Split slices',
min: 0,
max: null,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
editor: false,
params: [],
},
@ -98,7 +98,7 @@ export const samplePieVis = {
mustBeFirst: true,
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [
{
name: 'row',

View file

@ -80,7 +80,7 @@ export const getPieVisTypeDefinition = ({
}),
min: 0,
max: Infinity,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -91,7 +91,7 @@ export const getPieVisTypeDefinition = ({
mustBeFirst: true,
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -132,7 +132,7 @@ export const gaugeVisTypeDefinition: VisTypeDefinition<GaugeVisParams> = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -96,7 +96,7 @@ export const goalVisTypeDefinition: VisTypeDefinition<GaugeVisParams> = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -107,7 +107,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition<HeatmapVisParams> = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -115,7 +115,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition<HeatmapVisParams> = {
title: i18n.translate('visTypeVislib.heatmap.groupTitle', { defaultMessage: 'Y-axis' }),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -125,7 +125,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition<HeatmapVisParams> = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -625,7 +625,7 @@ export const getVis = (bucketType: string) => {
title: 'X-axis',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [],
},
{
@ -634,7 +634,7 @@ export const getVis = (bucketType: string) => {
title: 'Split series',
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [],
},
{
@ -643,7 +643,7 @@ export const getVis = (bucketType: string) => {
title: 'Split chart',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [
{
name: 'row',
@ -688,7 +688,7 @@ export const getVis = (bucketType: string) => {
title: 'X-axis',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [],
},
{
@ -697,7 +697,7 @@ export const getVis = (bucketType: string) => {
title: 'Split series',
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [],
},
{
@ -706,7 +706,7 @@ export const getVis = (bucketType: string) => {
title: 'Split chart',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [
{
name: 'row',
@ -722,7 +722,7 @@ export const getVis = (bucketType: string) => {
title: 'X-axis',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [],
},
{
@ -731,7 +731,7 @@ export const getVis = (bucketType: string) => {
title: 'Split series',
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [],
},
{
@ -740,7 +740,7 @@ export const getVis = (bucketType: string) => {
title: 'Split chart',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [
{
name: 'row',

View file

@ -149,7 +149,7 @@ export const sampleAreaVis = {
title: 'X-axis',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
editor: false,
params: [],
},
@ -159,7 +159,7 @@ export const sampleAreaVis = {
title: 'Split series',
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
editor: false,
params: [],
},
@ -169,7 +169,7 @@ export const sampleAreaVis = {
title: 'Split chart',
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
params: [
{
name: 'row',

View file

@ -155,7 +155,7 @@ export const areaVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -165,7 +165,7 @@ export const areaVisTypeDefinition = {
}),
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -175,7 +175,7 @@ export const areaVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -158,7 +158,7 @@ export const histogramVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -168,7 +168,7 @@ export const histogramVisTypeDefinition = {
}),
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -178,7 +178,7 @@ export const histogramVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -157,7 +157,7 @@ export const horizontalBarVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -167,7 +167,7 @@ export const horizontalBarVisTypeDefinition = {
}),
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -177,7 +177,7 @@ export const horizontalBarVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -149,7 +149,7 @@ export const lineVisTypeDefinition = {
title: i18n.translate('visTypeXy.line.segmentTitle', { defaultMessage: 'X-axis' }),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -159,7 +159,7 @@ export const lineVisTypeDefinition = {
}),
min: 0,
max: 3,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
{
group: AggGroupNames.Buckets,
@ -169,7 +169,7 @@ export const lineVisTypeDefinition = {
}),
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'],
},
],
},

View file

@ -0,0 +1,240 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { ExpectExpression, expectExpressionProvider } from './helpers';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
export default function ({
getService,
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
let expectExpression: ExpectExpression;
describe('esaggs multiterms tests', () => {
before(() => {
expectExpression = expectExpressionProvider({ getService, updateBaselines });
});
const timeRange = {
from: '2015-09-21T00:00:00Z',
to: '2015-09-22T00:00:00Z',
};
it('can execute multi terms', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggMultiTerms id="0" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=5}
aggs={aggCount id="1" enabled=true schema="metric"}
`;
const result = await expectExpression('esaggs_multi_terms', expression).getResponse();
expect(result.rows).to.eql([
{ 'col-0-0': { keys: ['jpg', 'CN'] }, 'col-1-1': 587 },
{ 'col-0-0': { keys: ['jpg', 'IN'] }, 'col-1-1': 472 },
{ 'col-0-0': { keys: ['jpg', 'US'] }, 'col-1-1': 253 },
{ 'col-0-0': { keys: ['css', 'CN'] }, 'col-1-1': 146 },
{ 'col-0-0': { keys: ['jpg', 'ID'] }, 'col-1-1': 105 },
]);
});
it('can include other bucket and ordering', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggMultiTerms id="0" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggCount id="1" enabled=true schema="metric"}
`;
const result = await expectExpression('esaggs_multi_terms_other', expression).getResponse();
expect(result.rows).to.eql([
{ 'col-0-0': { keys: ['png', 'GH'] }, 'col-1-1': 1 },
{ 'col-0-0': { keys: ['png', 'PE'] }, 'col-1-1': 2 },
{ 'col-0-0': { keys: ['png', 'CL'] }, 'col-1-1': 2 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': 4613 },
]);
});
it('can nest a regular terms', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggMultiTerms id="0" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggTerms id="1" enabled=true schema="bucket" field="geo.src" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggSum id="2" field="bytes" enabled=true schema="metric"}
`;
const result = await expectExpression('esaggs_multi_terms_nested', expression).getResponse();
expect(result.rows).to.eql([
{ 'col-0-0': { keys: ['png', 'GH'] }, 'col-1-1': 'IN', 'col-2-2': 18787 },
{ 'col-0-0': { keys: ['png', 'PE'] }, 'col-1-1': 'GT', 'col-2-2': 19328 },
{ 'col-0-0': { keys: ['png', 'PE'] }, 'col-1-1': 'BD', 'col-2-2': 18042 },
{ 'col-0-0': { keys: ['png', 'CL'] }, 'col-1-1': 'US', 'col-2-2': 19579 },
{ 'col-0-0': { keys: ['png', 'CL'] }, 'col-1-1': 'CN', 'col-2-2': 17681 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': 'DK', 'col-2-2': 20004 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': 'FI', 'col-2-2': 58508 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': 'QA', 'col-2-2': 9487 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': '__other__', 'col-2-2': 26417178 },
]);
});
it('can be nested into a regular terms', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggTerms id="0" enabled=true schema="bucket" field="geo.src" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggMultiTerms id="1" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggSum id="2" field="bytes" enabled=true schema="metric"}
`;
const result = await expectExpression('esaggs_multi_terms_nested2', expression).getResponse();
expect(result.rows).to.eql([
{ 'col-0-0': 'DK', 'col-1-1': { keys: ['png', 'IN'] }, 'col-2-2': 11004 },
{ 'col-0-0': 'DK', 'col-1-1': { keys: ['jpg', 'VN'] }, 'col-2-2': 9000 },
{ 'col-0-0': 'FI', 'col-1-1': { keys: ['png', 'CZ'] }, 'col-2-2': 16089 },
{ 'col-0-0': 'FI', 'col-1-1': { keys: ['png', 'MX'] }, 'col-2-2': 13360 },
{ 'col-0-0': 'FI', 'col-1-1': { keys: ['jpg', 'KH'] }, 'col-2-2': 8864 },
{ 'col-0-0': 'FI', 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 20195 },
{ 'col-0-0': 'QA', 'col-1-1': { keys: ['css', 'CL'] }, 'col-2-2': 9487 },
{ 'col-0-0': '__other__', 'col-1-1': { keys: ['png', 'GH'] }, 'col-2-2': 18787 },
{ 'col-0-0': '__other__', 'col-1-1': { keys: ['png', 'PE'] }, 'col-2-2': 37370 },
{ 'col-0-0': '__other__', 'col-1-1': { keys: ['png', 'CL'] }, 'col-2-2': 37260 },
{ 'col-0-0': '__other__', 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 26417178 },
]);
});
it('can be nested into itself', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggMultiTerms id="0" enabled=true schema="bucket" fields="geo.src" fields="host.raw" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggMultiTerms id="1" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggCount id="2" enabled=true schema="metric"}
`;
const result = await expectExpression('esaggs_multi_terms_nested3', expression).getResponse();
expect(result.rows).to.eql([
{
'col-0-0': {
keys: ['DK', 'media-for-the-masses.theacademyofperformingartsandscience.org'],
},
'col-1-1': { keys: ['png', 'IN'] },
'col-2-2': 1,
},
{
'col-0-0': {
keys: ['DK', 'media-for-the-masses.theacademyofperformingartsandscience.org'],
},
'col-1-1': { keys: ['jpg', 'VN'] },
'col-2-2': 1,
},
{
'col-0-0': { keys: ['GB', 'theacademyofperformingartsandscience.org'] },
'col-1-1': { keys: ['php', 'IN'] },
'col-2-2': 1,
},
{
'col-0-0': {
keys: ['KH', 'media-for-the-masses.theacademyofperformingartsandscience.org'],
},
'col-1-1': { keys: ['png', 'CN'] },
'col-2-2': 1,
},
{
'col-0-0': {
keys: ['KH', 'media-for-the-masses.theacademyofperformingartsandscience.org'],
},
'col-1-1': { keys: ['jpg', 'RO'] },
'col-2-2': 1,
},
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': { keys: ['png', 'GH'] }, 'col-2-2': 1 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': { keys: ['png', 'PE'] }, 'col-2-2': 2 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': { keys: ['png', 'CL'] }, 'col-2-2': 2 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 4608 },
]);
});
it('can be nested into date histogram', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggDateHistogram id="0" enabled=true schema="bucket" field="@timestamp" interval="6h"}
aggs={aggMultiTerms id="1" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=3 orderAgg={aggAvg id="order" field="bytes" enabled=true schema="metric"} otherBucket=true}
aggs={aggSum id="2" field="bytes" enabled=true schema="metric"}
`;
const result = await expectExpression('esaggs_multi_terms_nested4', expression).getResponse();
expect(result.rows).to.eql([
{ 'col-0-0': 1442781000000, 'col-1-1': { keys: ['png', 'IN'] }, 'col-2-2': 12097 },
{ 'col-0-0': 1442781000000, 'col-1-1': { keys: ['css', 'US'] }, 'col-2-2': 8768 },
{ 'col-0-0': 1442781000000, 'col-1-1': { keys: ['jpg', 'NG'] }, 'col-2-2': 8443 },
{ 'col-0-0': 1442781000000, 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 131961 },
{ 'col-0-0': 1442802600000, 'col-1-1': { keys: ['png', 'SN'] }, 'col-2-2': 17545 },
{ 'col-0-0': 1442802600000, 'col-1-1': { keys: ['png', 'PR'] }, 'col-2-2': 16719 },
{ 'col-0-0': 1442802600000, 'col-1-1': { keys: ['png', 'JP'] }, 'col-2-2': 32921 },
{ 'col-0-0': 1442802600000, 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 4146549 },
{ 'col-0-0': 1442824200000, 'col-1-1': { keys: ['png', 'GT'] }, 'col-2-2': 37794 },
{ 'col-0-0': 1442824200000, 'col-1-1': { keys: ['png', 'GH'] }, 'col-2-2': 18787 },
{ 'col-0-0': 1442824200000, 'col-1-1': { keys: ['png', 'PE'] }, 'col-2-2': 37370 },
{ 'col-0-0': 1442824200000, 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 15724462 },
{ 'col-0-0': 1442845800000, 'col-1-1': { keys: ['png', 'ZA'] }, 'col-2-2': 19659 },
{ 'col-0-0': 1442845800000, 'col-1-1': { keys: ['png', 'CL'] }, 'col-2-2': 19579 },
{ 'col-0-0': 1442845800000, 'col-1-1': { keys: ['png', 'BF'] }, 'col-2-2': 18448 },
{ 'col-0-0': 1442845800000, 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 6088917 },
{ 'col-0-0': 1442867400000, 'col-1-1': { keys: ['png', 'PL'] }, 'col-2-2': 14956 },
{ 'col-0-0': 1442867400000, 'col-1-1': { keys: ['jpg', 'BR'] }, 'col-2-2': 9474 },
{ 'col-0-0': 1442867400000, 'col-1-1': { keys: ['php', 'NL'] }, 'col-2-2': 9320 },
{ 'col-0-0': 1442867400000, 'col-1-1': { keys: ['__other__'] }, 'col-2-2': 224825 },
]);
});
it('can be used with filtered metric', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggMultiTerms id="0" enabled=true schema="bucket" fields="extension.raw" fields="geo.dest" size=3 orderAgg={aggFilteredMetric
id="order-1"
customBucket={aggFilter
id="order-1-filter"
enabled=true
schema="bucket"
filter={kql "geo.src:US"}
}
customMetric={aggSum id="order-2"
field="bytes"
enabled=true
schema="metric"
}
enabled=true
schema="metric"
} otherBucket=true}
aggs={aggFilteredMetric
id="1"
customBucket={aggFilter
id="1-filter"
enabled=true
schema="bucket"
filter={kql "geo.src:US"}
}
customMetric={aggSum id="2"
field="bytes"
enabled=true
schema="metric"
}
enabled=true
schema="metric"
}
`;
const result = await expectExpression(
'esaggs_multi_terms_filtered_metric',
expression
).getResponse();
expect(result.rows).to.eql([
{ 'col-0-0': { keys: ['jpg', 'IN'] }, 'col-1-1': 225557 },
{ 'col-0-0': { keys: ['jpg', 'CN'] }, 'col-1-1': 219324 },
{ 'col-0-0': { keys: ['jpg', 'US'] }, 'col-1-1': 106761 },
{ 'col-0-0': { keys: ['__other__'] }, 'col-1-1': 1649102 },
]);
});
});
}

View file

@ -43,5 +43,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
loadTestFile(require.resolve('./metric'));
loadTestFile(require.resolve('./esaggs'));
loadTestFile(require.resolve('./esaggs_timeshift'));
loadTestFile(require.resolve('./esaggs_multiterms'));
});
}