Skip to content

Commit

Permalink
[maps] Support time series split for top hits per entity source (#161799
Browse files Browse the repository at this point in the history
)

Closes #141978

<img width="600" alt="Screen Shot 2023-07-12 at 1 51 45 PM"
src="https://github.com/elastic/kibana/assets/373691/a71fc82f-31e0-49b2-9178-c70d890a9912">

### Test instructions
* clone https://github.com/thomasneirynck/faketracks
* cd into `faketracks`
* run `npm install`
* run `node ./generate_tracks.js --isTimeSeries`
* In Kibana, create `tracks` data view
* In Maps, create new map and add `Top hits` layer. Select `Tracks` data
view.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
nreese and kibanamachine authored Jul 14, 2023
1 parent d5bb2ad commit 2259e91
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
sortField: string;
sortOrder: SortDirection;
scalingType: SCALING_TYPES;
topHitsGroupByTimeseries: boolean;
topHitsSplitField: string;
topHitsSize: number;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('createLayerDescriptor', () => {
'client.geo.country_iso_code',
'client.as.organization.name',
],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: 'client.ip',
type: 'ES_SEARCH',
Expand Down Expand Up @@ -138,6 +139,7 @@ describe('createLayerDescriptor', () => {
'server.geo.country_iso_code',
'server.as.organization.name',
],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: 'server.ip',
type: 'ES_SEARCH',
Expand Down Expand Up @@ -290,6 +292,7 @@ describe('createLayerDescriptor', () => {
'source.geo.country_iso_code',
'source.as.organization.name',
],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: 'source.ip',
type: 'ES_SEARCH',
Expand Down Expand Up @@ -368,6 +371,7 @@ describe('createLayerDescriptor', () => {
'destination.geo.country_iso_code',
'destination.as.organization.name',
],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: 'destination.ip',
type: 'ES_SEARCH',
Expand Down Expand Up @@ -514,6 +518,7 @@ describe('createLayerDescriptor', () => {
'client.geo.country_iso_code',
'client.as.organization.name',
],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: 'client.ip',
type: 'ES_SEARCH',
Expand Down Expand Up @@ -592,6 +597,7 @@ describe('createLayerDescriptor', () => {
'server.geo.country_iso_code',
'server.as.organization.name',
],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: 'server.ip',
type: 'ES_SEARCH',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ test('Should create layer descriptor', () => {
sortField: '',
sortOrder: 'desc',
tooltipProperties: [],
topHitsGroupByTimeseries: false,
topHitsSize: 1,
topHitsSplitField: '',
type: 'ES_SEARCH',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { loadIndexSettings } from './util/load_index_settings';
import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
import { ESDocField } from '../../fields/es_doc_field';
import {
AbstractESSourceDescriptor,
DataRequestMeta,
ESSearchSourceDescriptor,
Timeslice,
Expand Down Expand Up @@ -83,6 +84,7 @@ type ESSearchSourceSyncMeta = Pick<
| 'sortField'
| 'sortOrder'
| 'scalingType'
| 'topHitsGroupByTimeseries'
| 'topHitsSplitField'
| 'topHitsSize'
>;
Expand All @@ -106,7 +108,9 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
protected readonly _tooltipFields: ESDocField[];

static createDescriptor(descriptor: Partial<ESSearchSourceDescriptor>): ESSearchSourceDescriptor {
const normalizedDescriptor = AbstractESSource.createDescriptor(descriptor);
const normalizedDescriptor = AbstractESSource.createDescriptor(
descriptor
) as AbstractESSourceDescriptor & Partial<ESSearchSourceDescriptor>;
if (!isValidStringConfig(normalizedDescriptor.geoField)) {
throw new Error('Cannot create an ESSearchSourceDescriptor without a geoField');
}
Expand All @@ -128,6 +132,10 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
scalingType: isValidStringConfig(descriptor.scalingType)
? descriptor.scalingType!
: SCALING_TYPES.MVT,
topHitsGroupByTimeseries:
typeof normalizedDescriptor.topHitsGroupByTimeseries === 'boolean'
? normalizedDescriptor.topHitsGroupByTimeseries
: false,
topHitsSplitField: isValidStringConfig(descriptor.topHitsSplitField)
? descriptor.topHitsSplitField!
: '',
Expand Down Expand Up @@ -168,6 +176,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
sortField={this._descriptor.sortField}
sortOrder={this._descriptor.sortOrder}
filterByMapBounds={this.isFilterByMapBounds()}
topHitsGroupByTimeseries={this._descriptor.topHitsGroupByTimeseries}
topHitsSplitField={this._descriptor.topHitsSplitField}
topHitsSize={this._descriptor.topHitsSize}
/>
Expand Down Expand Up @@ -271,9 +280,13 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
registerCancelCallback: (callback: () => void) => void,
inspectorAdapters: Adapters
) {
const { topHitsSplitField: topHitsSplitFieldName, topHitsSize } = this._descriptor;
const {
topHitsGroupByTimeseries,
topHitsSplitField: topHitsSplitFieldName,
topHitsSize,
} = this._descriptor;

if (!topHitsSplitFieldName) {
if (!topHitsGroupByTimeseries && !topHitsSplitFieldName) {
throw new Error('Cannot _getTopHits without topHitsSplitField');
}

Expand Down Expand Up @@ -310,7 +323,6 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
};
}

const topHitsSplitField: DataViewField = getField(indexPattern, topHitsSplitFieldName);
const cardinalityAgg = { precision_threshold: 1 };
const termsAgg = {
size: DEFAULT_MAX_BUCKETS_LIMIT,
Expand All @@ -319,26 +331,50 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource

const searchSource = await this.makeSearchSource(requestMeta, 0);
searchSource.setField('trackTotalHits', false);
searchSource.setField('aggs', {
totalEntities: {
cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField),
},
entitySplit: {
terms: addFieldToDSL(termsAgg, topHitsSplitField),
aggs: {
entityHits: {
top_hits: topHits,

if (topHitsGroupByTimeseries) {
searchSource.setField('aggs', {
totalEntities: {
cardinality: {
...cardinalityAgg,
field: '_tsid',
},
},
},
});
if (topHitsSplitField.type === 'string') {
const entityIsNotEmptyFilter = buildPhraseFilter(topHitsSplitField, '', indexPattern);
entityIsNotEmptyFilter.meta.negate = true;
searchSource.setField('filter', [
...(searchSource.getField('filter') as Filter[]),
entityIsNotEmptyFilter,
]);
entitySplit: {
terms: {
...termsAgg,
field: '_tsid',
},
aggs: {
entityHits: {
top_hits: topHits,
},
},
},
});
} else {
const topHitsSplitField: DataViewField = getField(indexPattern, topHitsSplitFieldName);
searchSource.setField('aggs', {
totalEntities: {
cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField),
},
entitySplit: {
terms: addFieldToDSL(termsAgg, topHitsSplitField),
aggs: {
entityHits: {
top_hits: topHits,
},
},
},
});
if (topHitsSplitField.type === 'string') {
const entityIsNotEmptyFilter = buildPhraseFilter(topHitsSplitField, '', indexPattern);
entityIsNotEmptyFilter.meta.negate = true;
searchSource.setField('filter', [
...(searchSource.getField('filter') as Filter[]),
entityIsNotEmptyFilter,
]);
}
}

const resp = await this._runEsQuery({
Expand All @@ -354,7 +390,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
'Get top hits from data view: {dataViewName}, entities: {entitiesFieldName}, geospatial field: {geoFieldName}',
values: {
dataViewName: indexPattern.getName(),
entitiesFieldName: topHitsSplitFieldName,
entitiesFieldName: topHitsGroupByTimeseries ? '_tsid' : topHitsSplitFieldName,
geoFieldName: this._descriptor.geoField,
},
}),
Expand Down Expand Up @@ -475,8 +511,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
}

_isTopHits(): boolean {
const { scalingType, topHitsSplitField } = this._descriptor;
return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField);
return this._descriptor.scalingType === SCALING_TYPES.TOP_HITS;
}

async _getSourceIndexList(): Promise<string[]> {
Expand Down Expand Up @@ -794,6 +829,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
sortField: this._descriptor.sortField,
sortOrder: this._descriptor.sortOrder,
scalingType: this._descriptor.scalingType,
topHitsGroupByTimeseries: this._descriptor.topHitsGroupByTimeseries,
topHitsSplitField: this._descriptor.topHitsSplitField,
topHitsSize: this._descriptor.topHitsSize,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { SortDirection } from '@kbn/data-plugin/public';
import { SCALING_TYPES } from '../../../../../common/constants';
import { GeoFieldSelect } from '../../../../components/geo_field_select';
import { GeoIndexPatternSelect } from '../../../../components/geo_index_pattern_select';
import { getGeoFields, getTermsFields, getSortFields } from '../../../../index_pattern_util';
import {
getGeoFields,
getTermsFields,
getSortFields,
getIsTimeseries,
} from '../../../../index_pattern_util';
import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types';
import { TopHitsForm } from './top_hits_form';
import { OnSourceChangeArgs } from '../../source';
Expand All @@ -27,40 +32,47 @@ interface Props {

interface State {
indexPattern: DataView | null;
isTimeseries: boolean;
geoFields: DataViewField[];
geoFieldName: string | null;
sortField: string | null;
sortFields: DataViewField[];
sortOrder: SortDirection;
termFields: DataViewField[];
topHitsGroupByTimeseries: boolean;
topHitsSplitField: string | null;
topHitsSize: number;
}

export class CreateSourceEditor extends Component<Props, State> {
state: State = {
indexPattern: null,
isTimeseries: false,
geoFields: [],
geoFieldName: null,
sortField: null,
sortFields: [],
sortOrder: SortDirection.desc,
termFields: [],
topHitsGroupByTimeseries: false,
topHitsSplitField: null,
topHitsSize: 1,
};

_onIndexPatternSelect = (indexPattern: DataView) => {
const geoFields = getGeoFields(indexPattern.fields);
const isTimeseries = getIsTimeseries(indexPattern);

this.setState(
{
indexPattern,
isTimeseries,
geoFields,
geoFieldName: geoFields.length ? geoFields[0].name : null,
sortField: indexPattern.timeFieldName ? indexPattern.timeFieldName : null,
sortFields: getSortFields(indexPattern.fields),
termFields: getTermsFields(indexPattern.fields),
topHitsGroupByTimeseries: isTimeseries,
topHitsSplitField: null,
},
this._previewLayer
Expand All @@ -80,11 +92,27 @@ export class CreateSourceEditor extends Component<Props, State> {
};

_previewLayer = () => {
const { indexPattern, geoFieldName, sortField, sortOrder, topHitsSplitField, topHitsSize } =
this.state;
const {
indexPattern,
geoFieldName,
sortField,
sortOrder,
topHitsGroupByTimeseries,
topHitsSplitField,
topHitsSize,
} = this.state;

const tooltipProperties: string[] = [];
if (topHitsSplitField) {
if (topHitsGroupByTimeseries) {
const timeSeriesDimensionFieldNames = (indexPattern?.fields ?? [])
.filter((field) => {
return field.timeSeriesDimension;
})
.map((field) => {
return field.name;
});
tooltipProperties.push(...timeSeriesDimensionFieldNames);
} else if (topHitsSplitField) {
tooltipProperties.push(topHitsSplitField);
}
if (indexPattern && indexPattern.timeFieldName) {
Expand All @@ -94,15 +122,16 @@ export class CreateSourceEditor extends Component<Props, State> {
const field = geoFieldName && indexPattern?.getFieldByName(geoFieldName);

const sourceConfig =
indexPattern && geoFieldName && sortField && topHitsSplitField
indexPattern && geoFieldName && sortField && (topHitsGroupByTimeseries || topHitsSplitField)
? {
indexPatternId: indexPattern.id,
geoField: geoFieldName,
scalingType: SCALING_TYPES.TOP_HITS,
sortField,
sortOrder,
tooltipProperties,
topHitsSplitField,
topHitsGroupByTimeseries,
topHitsSplitField: topHitsSplitField ? topHitsSplitField : undefined,
topHitsSize,
}
: null;
Expand All @@ -129,11 +158,13 @@ export class CreateSourceEditor extends Component<Props, State> {
<TopHitsForm
indexPatternId={this.state.indexPattern.id}
isColumnCompressed={false}
isTimeseries={this.state.isTimeseries}
onChange={this._onTopHitsPropChange}
sortField={this.state.sortField ? this.state.sortField : ''}
sortFields={this.state.sortFields}
sortOrder={this.state.sortOrder}
termFields={this.state.termFields}
topHitsGroupByTimeseries={this.state.topHitsGroupByTimeseries}
topHitsSplitField={this.state.topHitsSplitField}
topHitsSize={this.state.topHitsSize}
/>
Expand Down
Loading

0 comments on commit 2259e91

Please sign in to comment.