Skip to content

Commit

Permalink
[Search Sessions] Client side search cache (#92439)
Browse files Browse the repository at this point in the history
* dev docs

* sessions tutorial

* title

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Update dev_docs/tutorials/data/search.mdx

Co-authored-by: gchaps <[email protected]>

* Code review

* client cache

* mock utils

* improve code

* Use cacheOnClient in Lens

* mock

* docs and types

* unit tests!

* Search response cache + tests

* remove cacheOnClient
evict cache on error

* test ts

* shouldCacheOnClient + improve tests

* remove unused

* clear subs

* dont unsubscribe on setItem

* caching mess

* t

* fix jest

* add size to bfetch response @ppisljar
use it to reduce the # of stringify in response cache

* ts

* ts

* docs

* simplify abort controller logic and extract it into a class

* docs

* delete unused tests

* use addAbortSignal

* code review

* Use shareReplay, fix tests

* code review

* bfetch test

* code review

* Leave the bfetch changes out

* docs + isRestore

* make sure to clean up properly

* Make sure that aborting in cache works correctly
Clearer restructuring of code

* fix test

* import

* code review round 1

* ts

* Added functional test for search request caching

* test

* skip before codefreeze

Co-authored-by: gchaps <[email protected]>
Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
3 people committed Apr 16, 2021
1 parent ad6dc34 commit c0afedb
Show file tree
Hide file tree
Showing 19 changed files with 1,350 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) &gt; [getSerializableOptions](./kibana-plugin-plugins-data-public.searchinterceptor.getserializableoptions.md)

## SearchInterceptor.getSerializableOptions() method

<b>Signature:</b>

```typescript
protected getSerializableOptions(options?: ISearchOptions): Pick<ISearchOptions, "strategy" | "sessionId" | "isStored" | "isRestore" | "legacyHitsTotal">;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>ISearchOptions</code> | |

<b>Returns:</b>

`Pick<ISearchOptions, "strategy" | "sessionId" | "isStored" | "isRestore" | "legacyHitsTotal">`

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export declare class SearchInterceptor

| Method | Modifiers | Description |
| --- | --- | --- |
| [getSerializableOptions(options)](./kibana-plugin-plugins-data-public.searchinterceptor.getserializableoptions.md) | | |
| [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | |
| [handleSearchError(e, options, isTimeout)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | |
| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given <code>search</code> method. Overrides the <code>AbortSignal</code> with one that will abort either when the request times out, or when the original <code>AbortSignal</code> is aborted. Updates <code>pendingCount$</code> when the request is started/finalized. |
Expand Down
1 change: 1 addition & 0 deletions examples/search_examples/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface IMyStrategyRequest extends IEsSearchRequest {
}
export interface IMyStrategyResponse extends IEsSearchResponse {
cool: string;
executed_at: number;
}

export const SERVER_SEARCH_ROUTE_PATH = '/api/examples/search';
67 changes: 61 additions & 6 deletions examples/search_examples/public/search/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const SearchExamplesApp = ({
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
}, [fields]);

const doAsyncSearch = async (strategy?: string) => {
const doAsyncSearch = async (strategy?: string, sessionId?: string) => {
if (!indexPattern || !selectedNumericField) return;

// Construct the query portion of the search request
Expand All @@ -138,6 +138,7 @@ export const SearchExamplesApp = ({
const searchSubscription$ = data.search
.search(req, {
strategy,
sessionId,
})
.subscribe({
next: (res) => {
Expand All @@ -148,19 +149,30 @@ export const SearchExamplesApp = ({
? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
res.rawResponse.aggregations[1].value
: undefined;
const isCool = (res as IMyStrategyResponse).cool;
const executedAt = (res as IMyStrategyResponse).executed_at;
const message = (
<EuiText>
Searched {res.rawResponse.hits.total} documents. <br />
The average of {selectedNumericField!.name} is{' '}
{avgResult ? Math.floor(avgResult) : 0}.
<br />
Is it Cool? {String((res as IMyStrategyResponse).cool)}
{isCool ? `Is it Cool? ${isCool}` : undefined}
<br />
<EuiText data-test-subj="requestExecutedAt">
{executedAt ? `Executed at? ${executedAt}` : undefined}
</EuiText>
</EuiText>
);
notifications.toasts.addSuccess({
title: 'Query result',
text: mountReactNode(message),
});
notifications.toasts.addSuccess(
{
title: 'Query result',
text: mountReactNode(message),
},
{
toastLifeTimeMs: 300000,
}
);
searchSubscription$.unsubscribe();
} else if (isErrorResponse(res)) {
// TODO: Make response error status clearer
Expand Down Expand Up @@ -227,6 +239,10 @@ export const SearchExamplesApp = ({
doAsyncSearch('myStrategy');
};

const onClientSideSessionCacheClickHandler = () => {
doAsyncSearch('myStrategy', data.search.session.getSessionId());
};

const onServerClickHandler = async () => {
if (!indexPattern || !selectedNumericField) return;
try {
Expand Down Expand Up @@ -374,6 +390,45 @@ export const SearchExamplesApp = ({
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Client side search session caching</h3>
</EuiTitle>
<EuiText>
<EuiButtonEmpty
size="xs"
onClick={() => data.search.session.start()}
iconType="alert"
data-test-subj="searchExamplesStartSession"
>
<FormattedMessage
id="searchExamples.startNewSession"
defaultMessage="Start a new session"
/>
</EuiButtonEmpty>
<EuiButtonEmpty
size="xs"
onClick={() => data.search.session.clear()}
iconType="alert"
data-test-subj="searchExamplesClearSession"
>
<FormattedMessage
id="searchExamples.clearSession"
defaultMessage="Clear session"
/>
</EuiButtonEmpty>
<EuiButtonEmpty
size="xs"
onClick={onClientSideSessionCacheClickHandler}
iconType="play"
data-test-subj="searchExamplesCacheSearch"
>
<FormattedMessage
id="searchExamples.myStrategyButtonText"
defaultMessage="Request from low-level client via My Strategy"
/>
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Using search on the server</h3>
</EuiTitle>
Expand Down
1 change: 1 addition & 0 deletions examples/search_examples/server/my_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const mySearchStrategyProvider = (
map((esSearchRes) => ({
...esSearchRes,
cool: request.get_cool ? 'YES' : 'NOPE',
executed_at: new Date().getTime(),
}))
),
cancel: async (id, options, deps) => {
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/data/common/search/tabify/tabify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function tabifyAggResponse(
const write = new TabbedAggResponseWriter(aggConfigs, respOpts || {});
const topLevelBucket: AggResponseBucket = {
...esResponse.aggregations,
doc_count: esResponse.hits.total,
doc_count: esResponse.hits?.total,
};

collectBucket(aggConfigs, write, topLevelBucket, '', 1);
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2361,6 +2361,8 @@ export class SearchInterceptor {
// (undocumented)
protected readonly deps: SearchInterceptorDeps;
// (undocumented)
protected getSerializableOptions(options?: ISearchOptions): Pick<ISearchOptions, "strategy" | "sessionId" | "isStored" | "isRestore" | "legacyHitsTotal">;
// (undocumented)
protected getTimeoutMode(): TimeoutErrorMode;
// Warning: (ae-forgotten-export) The symbol "KibanaServerError" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "AbortError" needs to be exported by the entry point index.d.ts
Expand Down
28 changes: 17 additions & 11 deletions src/plugins/data/public/search/search_interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,14 @@ export class SearchInterceptor {
}
}

/**
* @internal
* @throws `AbortError` | `ErrorLike`
*/
protected runSearch(
request: IKibanaSearchRequest,
options?: ISearchOptions
): Promise<IKibanaSearchResponse> {
const { abortSignal, sessionId, ...requestOptions } = options || {};
protected getSerializableOptions(options?: ISearchOptions) {
const { sessionId, ...requestOptions } = options || {};

const serializableOptions: ISearchOptionsSerializable = {};
const combined = {
...requestOptions,
...this.deps.session.getSearchOptions(sessionId),
};
const serializableOptions: ISearchOptionsSerializable = {};

if (combined.sessionId !== undefined) serializableOptions.sessionId = combined.sessionId;
if (combined.isRestore !== undefined) serializableOptions.isRestore = combined.isRestore;
Expand All @@ -135,10 +129,22 @@ export class SearchInterceptor {
if (combined.strategy !== undefined) serializableOptions.strategy = combined.strategy;
if (combined.isStored !== undefined) serializableOptions.isStored = combined.isStored;

return serializableOptions;
}

/**
* @internal
* @throws `AbortError` | `ErrorLike`
*/
protected runSearch(
request: IKibanaSearchRequest,
options?: ISearchOptions
): Promise<IKibanaSearchResponse> {
const { abortSignal } = options || {};
return this.batchedFetch(
{
request,
options: serializableOptions,
options: this.getSerializableOptions(options),
},
abortSignal
);
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/data/public/search/session/session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export interface SearchSessionIndicatorUiConfig {
}

/**
* Responsible for tracking a current search session. Supports only a single session at a time.
* Responsible for tracking a current search session. Supports a single session at a time.
*/
export class SessionService {
public readonly state$: Observable<SearchSessionState>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,24 @@ describe('search abort controller', () => {
test('immediately aborts when passed an aborted signal in the constructor', () => {
const controller = new AbortController();
controller.abort();
const sac = new SearchAbortController(controller.signal);
const sac = new SearchAbortController();
sac.addAbortSignal(controller.signal);
expect(sac.getSignal().aborted).toBe(true);
});

test('aborts when input signal is aborted', () => {
const controller = new AbortController();
const sac = new SearchAbortController(controller.signal);
const sac = new SearchAbortController();
sac.addAbortSignal(controller.signal);
expect(sac.getSignal().aborted).toBe(false);
controller.abort();
expect(sac.getSignal().aborted).toBe(true);
});

test('aborts when all input signals are aborted', () => {
const controller = new AbortController();
const sac = new SearchAbortController(controller.signal);
const sac = new SearchAbortController();
sac.addAbortSignal(controller.signal);

const controller2 = new AbortController();
sac.addAbortSignal(controller2.signal);
Expand All @@ -48,7 +51,8 @@ describe('search abort controller', () => {

test('aborts explicitly even if all inputs are not aborted', () => {
const controller = new AbortController();
const sac = new SearchAbortController(controller.signal);
const sac = new SearchAbortController();
sac.addAbortSignal(controller.signal);

const controller2 = new AbortController();
sac.addAbortSignal(controller2.signal);
Expand All @@ -60,7 +64,8 @@ describe('search abort controller', () => {

test('doesnt abort, if cleared', () => {
const controller = new AbortController();
const sac = new SearchAbortController(controller.signal);
const sac = new SearchAbortController();
sac.addAbortSignal(controller.signal);
expect(sac.getSignal().aborted).toBe(false);
sac.cleanup();
controller.abort();
Expand All @@ -77,15 +82,15 @@ describe('search abort controller', () => {
});

test('doesnt abort on timeout, if cleared', () => {
const sac = new SearchAbortController(undefined, 100);
const sac = new SearchAbortController(100);
expect(sac.getSignal().aborted).toBe(false);
sac.cleanup();
timeTravel(100);
expect(sac.getSignal().aborted).toBe(false);
});

test('aborts on timeout, even if no signals passed in', () => {
const sac = new SearchAbortController(undefined, 100);
const sac = new SearchAbortController(100);
expect(sac.getSignal().aborted).toBe(false);
timeTravel(100);
expect(sac.getSignal().aborted).toBe(true);
Expand All @@ -94,7 +99,8 @@ describe('search abort controller', () => {

test('aborts on timeout, even if there are unaborted signals', () => {
const controller = new AbortController();
const sac = new SearchAbortController(controller.signal, 100);
const sac = new SearchAbortController(100);
sac.addAbortSignal(controller.signal);

expect(sac.getSignal().aborted).toBe(false);
timeTravel(100);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ export class SearchAbortController {
private destroyed = false;
private reason?: AbortReason;

constructor(abortSignal?: AbortSignal, timeout?: number) {
if (abortSignal) {
this.addAbortSignal(abortSignal);
}

constructor(timeout?: number) {
if (timeout) {
this.timeoutSub = timer(timeout).subscribe(() => {
this.reason = AbortReason.Timeout;
Expand All @@ -41,6 +37,7 @@ export class SearchAbortController {
};

public cleanup() {
if (this.destroyed) return;
this.destroyed = true;
this.timeoutSub?.unsubscribe();
this.inputAbortSignals.forEach((abortSignal) => {
Expand Down
Loading

0 comments on commit c0afedb

Please sign in to comment.