mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Multi field terms (#116928)
This commit is contained in:
parent
7bd903bd16
commit
079db9666e
31 changed files with 1133 additions and 225 deletions
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
191
src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts
Normal file
191
src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts
Normal 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' });
|
||||
});
|
||||
});
|
147
src/plugins/data/common/search/aggs/buckets/multi_terms.ts
Normal file
147
src/plugins/data/common/search/aggs/buckets/multi_terms.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
132
src/plugins/data/common/search/aggs/buckets/multi_terms_fn.ts
Normal file
132
src/plugins/data/common/search/aggs/buckets/multi_terms_fn.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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',
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue