diff --git a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts index af268600902562..1325fb651ec507 100644 --- a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts +++ b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts @@ -13,6 +13,7 @@ import { logEntryCursorRT, logEntryRT, } from '../../log_entry'; +import { jsonObjectRT } from '../../typed_json'; import { searchStrategyErrorRT } from '../common/errors'; export const LOG_ENTRIES_SEARCH_STRATEGY = 'infra-log-entries'; @@ -22,11 +23,12 @@ const logEntriesBaseSearchRequestParamsRT = rt.intersection([ sourceId: rt.string, startTimestamp: rt.number, endTimestamp: rt.number, + size: rt.number, }), rt.partial({ - query: rt.union([rt.string, rt.null]), - size: rt.number, + query: jsonObjectRT, columns: rt.array(logSourceColumnConfigurationRT), + highlightTerms: rt.array(rt.string), }), ]); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts index c8aaef535dde25..0e293d430f37cc 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from '@kbn/std'; import * as rt from 'io-ts'; import { concat, defer, of } from 'rxjs'; import { concatMap, filter, map, shareReplay, take } from 'rxjs/operators'; @@ -68,7 +69,18 @@ export const logEntriesSearchStrategyProvider = ({ sourceConfiguration$.pipe( map( ({ configuration }): IEsSearchRequest => ({ - params: createGetLogEntriesQuery(configuration.logAlias), + params: createGetLogEntriesQuery( + configuration.logAlias, + params.startTimestamp, + params.endTimestamp, + undefined, // TODO: determine cursor + params.size, + configuration.fields.timestamp, + configuration.fields.tiebreaker, + [], // TODO: determine fields list + params.query, + undefined // TODO: map over highlight terms OR reduce to one term in request + ), }) ) ) diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/common.ts b/x-pack/plugins/infra/server/services/log_entries/queries/common.ts new file mode 100644 index 00000000000000..f170fa337a8b9e --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/queries/common.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const createSortClause = ( + sortDirection: 'asc' | 'desc', + timestampField: string, + tiebreakerField: string +) => ({ + sort: { + [timestampField]: sortDirection, + [tiebreakerField]: sortDirection, + }, +}); + +export const createTimeRangeFilterClauses = ( + startTimestamp: number, + endTimestamp: number, + timestampField: string +) => [ + { + range: { + [timestampField]: { + gte: startTimestamp, + lte: endTimestamp, + format: 'epoch_millis', + }, + }, + }, +]; diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts b/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts index c2b9d2bc57e437..c727bb7d808601 100644 --- a/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts +++ b/x-pack/plugins/infra/server/services/log_entries/queries/log_entries.ts @@ -8,14 +8,16 @@ import type { RequestParams } from '@elastic/elasticsearch'; import * as rt from 'io-ts'; import { LogEntryAfterCursor, + logEntryAfterCursorRT, LogEntryBeforeCursor, - LogEntryCursor, + logEntryBeforeCursorRT, } from '../../../../common/log_entry'; import { jsonArrayRT, JsonObject } from '../../../../common/typed_json'; import { commonHitFieldsRT, commonSearchSuccessResponseFieldsRT, } from '../../../utils/elasticsearch_runtime_types'; +import { createSortClause, createTimeRangeFilterClauses } from './common'; export const createGetLogEntriesQuery = ( logEntriesIndex: string, @@ -28,50 +30,98 @@ export const createGetLogEntriesQuery = ( fields: string[], query?: JsonObject, highlightTerm?: string -): RequestParams.AsyncSearchSubmit> => ({ - index: logEntriesIndex, - terminate_after: 1, - track_scores: false, - track_total_hits: false, - body: { - size: 1, - query: {}, - fields: ['*'], - // sort: [{ [timestampField]: 'desc' }, { [tiebreakerField]: 'desc' }], - _source: false, - }, -}); +): RequestParams.AsyncSearchSubmit> => { + const sortDirection = getSortDirection(cursor); + const highlightQuery = createHighlightQuery(highlightTerm, fields); -function createSortAndSearchAfterClause( + return { + index: logEntriesIndex, + allow_no_indices: true, + terminate_after: 1, + track_scores: false, + track_total_hits: false, + body: { + size, + query: { + bool: { + filter: [ + ...(query ? [query] : []), + ...(highlightQuery ? [highlightQuery] : []), + ...createTimeRangeFilterClauses(startTimestamp, endTimestamp, timestampField), + ], + }, + }, + fields, + _source: false, + ...createSortClause(sortDirection, timestampField, tiebreakerField), + ...createSearchAfterClause(cursor), + ...createHighlightClause(highlightQuery, fields), + }, + }; +}; + +const getSortDirection = ( + cursor: LogEntryBeforeCursor | LogEntryAfterCursor | null | undefined +): 'asc' | 'desc' => (logEntryBeforeCursorRT.is(cursor) ? 'desc' : 'asc'); + +const createSearchAfterClause = ( cursor: LogEntryBeforeCursor | LogEntryAfterCursor | null | undefined -): { - sortDirection: 'asc' | 'desc'; - searchAfterClause: { search_after?: readonly [number, number] }; -} { - if (cursor) { - if ('before' in cursor) { - return { - sortDirection: 'desc', - searchAfterClause: - cursor.before !== 'last' - ? { search_after: [cursor.before.time, cursor.before.tiebreaker] as const } - : {}, - }; - } else if (cursor.after !== 'first') { - return { - sortDirection: 'asc', - searchAfterClause: { search_after: [cursor.after.time, cursor.after.tiebreaker] as const }, - }; - } +): { search_after?: [number, number] } => { + if (logEntryBeforeCursorRT.is(cursor) && cursor.before !== 'last') { + return { + search_after: [cursor.before.time, cursor.before.tiebreaker], + }; + } else if (logEntryAfterCursorRT.is(cursor) && cursor.after !== 'first') { + return { + search_after: [cursor.after.time, cursor.after.tiebreaker], + }; } - return { sortDirection: 'asc', searchAfterClause: {} }; -} + return {}; +}; + +const createHighlightClause = (highlightQuery: JsonObject | undefined, fields: string[]) => + highlightQuery + ? { + highlight: { + boundary_scanner: 'word', + fields: fields.reduce( + (highlightFieldConfigs, fieldName) => ({ + ...highlightFieldConfigs, + [fieldName]: {}, + }), + {} + ), + fragment_size: 1, + number_of_fragments: 100, + post_tags: [''], + pre_tags: [''], + highlight_query: highlightQuery, + }, + } + : {}; + +const createHighlightQuery = ( + highlightTerm: string | undefined, + fields: string[] +): JsonObject | undefined => { + if (highlightTerm) { + return { + multi_match: { + fields, + lenient: true, + query: highlightTerm, + type: 'phrase', + }, + }; + } +}; export const logEntryHitRT = rt.intersection([ commonHitFieldsRT, rt.type({ fields: rt.record(rt.string, jsonArrayRT), + highlight: rt.record(rt.string, rt.array(rt.string)), sort: rt.tuple([rt.number, rt.number]), }), ]);