From 8f6e7e0226b1b7fe1a15c0911e45f50bdb11bb74 Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Sat, 11 May 2024 12:46:32 +0200 Subject: [PATCH] support sending aggregations --- .../QueryEditor/AggregationEditor.tsx | 2 +- .../quickwit-ui/src/services/client.test.ts | 2 +- quickwit/quickwit-ui/src/services/client.ts | 118 ++++++++++++++---- quickwit/quickwit-ui/src/utils/models.ts | 1 + quickwit/quickwit-ui/src/views/SearchView.tsx | 3 +- 5 files changed, 100 insertions(+), 26 deletions(-) diff --git a/quickwit/quickwit-ui/src/components/QueryEditor/AggregationEditor.tsx b/quickwit/quickwit-ui/src/components/QueryEditor/AggregationEditor.tsx index d72155ac4f2..a0a5a712700 100644 --- a/quickwit/quickwit-ui/src/components/QueryEditor/AggregationEditor.tsx +++ b/quickwit/quickwit-ui/src/components/QueryEditor/AggregationEditor.tsx @@ -79,7 +79,7 @@ export function MetricKind(props: SearchComponentProps) { sx={{ "min-height": "44px" }} > Count - Average + Average Sum Max Min diff --git a/quickwit/quickwit-ui/src/services/client.test.ts b/quickwit/quickwit-ui/src/services/client.test.ts index 2a0aaa17f3e..7a243a089bd 100644 --- a/quickwit/quickwit-ui/src/services/client.test.ts +++ b/quickwit/quickwit-ui/src/services/client.test.ts @@ -39,6 +39,6 @@ describe('Client unit test', () => { histogram: null, }, }; - expect(new Client().buildSearchUrl(searchRequest).toString()).toBe("http://localhost/api/v1/my-new-fresh-index-id/search?query=severity_error%3AERROR&max_hits=20&start_timestamp=100&end_timestamp=200&sort_by_field=%2Btimestamp"); + expect(new Client().buildSearchBody(searchRequest, null)).toBe('{"query":"severity_error:ERROR","max_hits":20,"start_timestamp":100,"end_timestamp":200,"sort_by_field":"+timestamp"}'); }); }); diff --git a/quickwit/quickwit-ui/src/services/client.ts b/quickwit/quickwit-ui/src/services/client.ts index 90e3a56e8ad..bbaa4fe0ff3 100644 --- a/quickwit/quickwit-ui/src/services/client.ts +++ b/quickwit/quickwit-ui/src/services/client.ts @@ -35,13 +35,14 @@ export class Client { return this._host + "/api/v1/"; } - async search(request: SearchRequest): Promise { + async search(request: SearchRequest, timestamp_field: string | null): Promise { // TODO: improve validation of request. if (request.indexId === null || request.indexId === undefined) { throw Error("Search request must have and index id.") } - const url = this.buildSearchUrl(request); - return this.fetch(url.toString(), this.defaultGetRequestParams()); + const url = `${this.apiRoot()}/${request.indexId}/search`; + const body = this.buildSearchBody(request, timestamp_field); + return this.fetch(url, this.defaultGetRequestParams(), body); } async cluster(): Promise { @@ -85,7 +86,12 @@ export class Client { return this.fetch(`${this.apiRoot()}indexes`, {}); } - async fetch(url: string, params: RequestInit): Promise { + async fetch(url: string, params: RequestInit, body: string|null = null): Promise { + if (body !== null) { + params.method = "POST"; + params.body = body; + params.headers = Object.assign({}, params.headers, {"content-type": "application/json"}); + } const response = await fetch(url, params); if (response.ok) { return response.json() as Promise; @@ -101,34 +107,100 @@ export class Client { return { method: "GET", headers: { Accept: "application/json" }, - mode: "no-cors", + mode: "cors", cache: "default", } } - buildSearchUrl(request: SearchRequest): URL { - const url: URL = new URL(`${request.indexId}/search`, this.apiRoot()); - // TODO: the trim should be done in the backend. - url.searchParams.append("query", request.query.trim() || "*"); - url.searchParams.append("max_hits", "20"); + buildSearchBody(request: SearchRequest, timestamp_field: string | null): string { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const body: any = { + // TODO: the trim should be done in the backend. + query: request.query.trim() || "*", + }; + + if (request.aggregation) { + const qw_aggregation = this.buildAggregation(request, timestamp_field); + body["aggs"] = qw_aggregation; + body["max_hits"] = 0; + } else { + body["max_hits"] = 20; + } if (request.startTimestamp) { - url.searchParams.append( - "start_timestamp", - request.startTimestamp.toString() - ); + body["start_timestamp"] = request.startTimestamp; } if (request.endTimestamp) { - url.searchParams.append( - "end_timestamp", - request.endTimestamp.toString() - ); + body["end_timestamp"] = request.endTimestamp; } if (request.sortByField) { - url.searchParams.append( - "sort_by_field", - serializeSortByField(request.sortByField) - ); + body["sort_by_field"] = serializeSortByField(request.sortByField); + } + return JSON.stringify(body); + } + + /* eslint-disable @typescript-eslint/no-explicit-any */ + buildAggregation(request: SearchRequest, timestamp_field: string | null): any { + let aggregation = undefined; + if (request.aggregationConfig.metric) { + const metric = request.aggregationConfig.metric; + aggregation = { + metric: { + [metric.type]: { + field: metric.field + } + } + } + } + if (request.aggregationConfig.histogram && timestamp_field) { + const histogram = request.aggregationConfig.histogram; + let auto; + if (request.endTimestamp == null || request.startTimestamp == null) { + auto = "1d"; + } else if ((request.endTimestamp - request.startTimestamp) > 7 * 24 * 60 * 60) { + auto = "1d"; + } else if ((request.endTimestamp - request.startTimestamp) > 24 * 60 * 60) { + auto = "10m" + } else { + auto = "1m" + } + const interval = histogram.interval !== "auto" ? histogram.interval : auto; + let extended_bounds; + if (request.startTimestamp && request.endTimestamp) { + extended_bounds = { + min: request.startTimestamp, + max: request.endTimestamp, + }; + } else { + extended_bounds = undefined; + } + aggregation = { + histo_agg: { + aggs: aggregation, + date_histogram: { + field: timestamp_field, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: extended_bounds, + } + } + } + } + if (request.aggregationConfig.term) { + const term = request.aggregationConfig.term; + aggregation = { + term_agg: { + aggs: aggregation, + terms: { + field: term.field, + size: term.size, + order: { + _count: "desc" + }, + min_doc_count: 1, + } + } + } } - return url; + return aggregation; } } diff --git a/quickwit/quickwit-ui/src/utils/models.ts b/quickwit/quickwit-ui/src/utils/models.ts index 48276f041f1..49127fb8c38 100644 --- a/quickwit/quickwit-ui/src/utils/models.ts +++ b/quickwit/quickwit-ui/src/utils/models.ts @@ -135,6 +135,7 @@ export type SearchResponse = { hits: Array; elapsed_time_micros: number; errors: Array | undefined; + aggregations: any | undefined; } export type IndexConfig = { diff --git a/quickwit/quickwit-ui/src/views/SearchView.tsx b/quickwit/quickwit-ui/src/views/SearchView.tsx index ab180ec5f88..72e35534f5c 100644 --- a/quickwit/quickwit-ui/src/views/SearchView.tsx +++ b/quickwit/quickwit-ui/src/views/SearchView.tsx @@ -71,7 +71,8 @@ function SearchView() { setQueryRunning(true); setSearchError(null); navigate('/search?' + toUrlSearchRequestParams(updatedSearchRequest).toString()); - quickwitClient.search(updatedSearchRequest).then((response) => { + const timestamp_field = index?.metadata.index_config.doc_mapping.timestamp_field || null; + quickwitClient.search(updatedSearchRequest, timestamp_field).then((response) => { updateLastSearchRequest(updatedSearchRequest); setSearchResponse(response); setQueryRunning(false);