-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…103798) * Migrate kibana.autocomplete config to data plugin * Fix CI * Fix tests * Use new terms enum API for autocomplete value suggestions * Add tiers to config * Re-introduce terms agg and add config/tests for swapping algorithms * Add data_content and data_cold tiers by default * Fix types * Fix maps test * Update tests Co-authored-by: Kibana Machine <[email protected]> # Conflicts: # src/plugins/data/server/autocomplete/value_suggestions_route.ts
- Loading branch information
1 parent
969de56
commit 38295a7
Showing
7 changed files
with
372 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
* 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 { coreMock } from '../../../../core/server/mocks'; | ||
import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; | ||
import { ConfigSchema } from '../../config'; | ||
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; | ||
import type { ApiResponse } from '@elastic/elasticsearch'; | ||
import { termsAggSuggestions } from './terms_agg'; | ||
import { SearchResponse } from 'elasticsearch'; | ||
import { duration } from 'moment'; | ||
|
||
let savedObjectsClientMock: jest.Mocked<SavedObjectsClientContract>; | ||
let esClientMock: DeeplyMockedKeys<ElasticsearchClient>; | ||
const configMock = ({ | ||
autocomplete: { | ||
valueSuggestions: { timeout: duration(4513), terminateAfter: duration(98430) }, | ||
}, | ||
} as unknown) as ConfigSchema; | ||
const mockResponse = { | ||
body: { | ||
aggregations: { | ||
suggestions: { | ||
buckets: [{ key: 'whoa' }, { key: 'amazing' }], | ||
}, | ||
}, | ||
}, | ||
} as ApiResponse<SearchResponse<any>>; | ||
|
||
describe('terms agg suggestions', () => { | ||
beforeEach(() => { | ||
const requestHandlerContext = coreMock.createRequestHandlerContext(); | ||
savedObjectsClientMock = requestHandlerContext.savedObjects.client; | ||
esClientMock = requestHandlerContext.elasticsearch.client.asCurrentUser; | ||
esClientMock.search.mockResolvedValue(mockResponse); | ||
}); | ||
|
||
it('calls the _search API with a terms agg with the given args', async () => { | ||
const result = await termsAggSuggestions( | ||
configMock, | ||
savedObjectsClientMock, | ||
esClientMock, | ||
'index', | ||
'fieldName', | ||
'query', | ||
[], | ||
{ name: 'field_name', type: 'string' } | ||
); | ||
|
||
const [[args]] = esClientMock.search.mock.calls; | ||
|
||
expect(args).toMatchInlineSnapshot(` | ||
Object { | ||
"body": Object { | ||
"aggs": Object { | ||
"suggestions": Object { | ||
"terms": Object { | ||
"execution_hint": "map", | ||
"field": "field_name", | ||
"include": "query.*", | ||
"shard_size": 10, | ||
}, | ||
}, | ||
}, | ||
"query": Object { | ||
"bool": Object { | ||
"filter": Array [], | ||
}, | ||
}, | ||
"size": 0, | ||
"terminate_after": 98430, | ||
"timeout": "4513ms", | ||
}, | ||
"index": "index", | ||
} | ||
`); | ||
expect(result).toMatchInlineSnapshot(` | ||
Array [ | ||
"whoa", | ||
"amazing", | ||
] | ||
`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* 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 { get, map } from 'lodash'; | ||
import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; | ||
import { estypes } from '@elastic/elasticsearch'; | ||
import { ConfigSchema } from '../../config'; | ||
import { IFieldType } from '../../common'; | ||
import { findIndexPatternById, getFieldByName } from '../index_patterns'; | ||
import { shimAbortSignal } from '../search'; | ||
|
||
export async function termsAggSuggestions( | ||
config: ConfigSchema, | ||
savedObjectsClient: SavedObjectsClientContract, | ||
esClient: ElasticsearchClient, | ||
index: string, | ||
fieldName: string, | ||
query: string, | ||
filters?: estypes.QueryDslQueryContainer[], | ||
field?: IFieldType, | ||
abortSignal?: AbortSignal | ||
) { | ||
const autocompleteSearchOptions = { | ||
timeout: `${config.autocomplete.valueSuggestions.timeout.asMilliseconds()}ms`, | ||
terminate_after: config.autocomplete.valueSuggestions.terminateAfter.asMilliseconds(), | ||
}; | ||
|
||
if (!field?.name && !field?.type) { | ||
const indexPattern = await findIndexPatternById(savedObjectsClient, index); | ||
|
||
field = indexPattern && getFieldByName(fieldName, indexPattern); | ||
} | ||
|
||
const body = await getBody(autocompleteSearchOptions, field ?? fieldName, query, filters); | ||
|
||
const promise = esClient.search({ index, body }); | ||
const result = await shimAbortSignal(promise, abortSignal); | ||
|
||
const buckets = | ||
get(result.body, 'aggregations.suggestions.buckets') || | ||
get(result.body, 'aggregations.nestedSuggestions.suggestions.buckets'); | ||
|
||
return map(buckets ?? [], 'key'); | ||
} | ||
|
||
async function getBody( | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
{ timeout, terminate_after }: Record<string, any>, | ||
field: IFieldType | string, | ||
query: string, | ||
filters: estypes.QueryDslQueryContainer[] = [] | ||
) { | ||
const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name); | ||
|
||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators | ||
const getEscapedQuery = (q: string = '') => | ||
q.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, (match) => `\\${match}`); | ||
|
||
// Helps ensure that the regex is not evaluated eagerly against the terms dictionary | ||
const executionHint = 'map' as const; | ||
|
||
// We don't care about the accuracy of the counts, just the content of the terms, so this reduces | ||
// the amount of information that needs to be transmitted to the coordinating node | ||
const shardSize = 10; | ||
const body = { | ||
size: 0, | ||
timeout, | ||
terminate_after, | ||
query: { | ||
bool: { | ||
filter: filters, | ||
}, | ||
}, | ||
aggs: { | ||
suggestions: { | ||
terms: { | ||
field: isFieldObject(field) ? field.name : field, | ||
include: `${getEscapedQuery(query)}.*`, | ||
execution_hint: executionHint, | ||
shard_size: shardSize, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
if (isFieldObject(field) && field.subType && field.subType.nested) { | ||
return { | ||
...body, | ||
aggs: { | ||
nestedSuggestions: { | ||
nested: { | ||
path: field.subType.nested.path, | ||
}, | ||
aggs: body.aggs, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
return body; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* 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 { termsEnumSuggestions } from './terms_enum'; | ||
import { coreMock } from '../../../../core/server/mocks'; | ||
import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; | ||
import { ConfigSchema } from '../../config'; | ||
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; | ||
import type { ApiResponse } from '@elastic/elasticsearch'; | ||
|
||
let savedObjectsClientMock: jest.Mocked<SavedObjectsClientContract>; | ||
let esClientMock: DeeplyMockedKeys<ElasticsearchClient>; | ||
const configMock = { | ||
autocomplete: { valueSuggestions: { tiers: ['data_hot', 'data_warm', 'data_content'] } }, | ||
} as ConfigSchema; | ||
const mockResponse = { | ||
body: { terms: ['whoa', 'amazing'] }, | ||
}; | ||
|
||
describe('_terms_enum suggestions', () => { | ||
beforeEach(() => { | ||
const requestHandlerContext = coreMock.createRequestHandlerContext(); | ||
savedObjectsClientMock = requestHandlerContext.savedObjects.client; | ||
esClientMock = requestHandlerContext.elasticsearch.client.asCurrentUser; | ||
esClientMock.transport.request.mockResolvedValue((mockResponse as unknown) as ApiResponse); | ||
}); | ||
|
||
it('calls the _terms_enum API with the field, query, filters, and config tiers', async () => { | ||
const result = await termsEnumSuggestions( | ||
configMock, | ||
savedObjectsClientMock, | ||
esClientMock, | ||
'index', | ||
'fieldName', | ||
'query', | ||
[], | ||
{ name: 'field_name', type: 'string' } | ||
); | ||
|
||
const [[args]] = esClientMock.transport.request.mock.calls; | ||
|
||
expect(args).toMatchInlineSnapshot(` | ||
Object { | ||
"body": Object { | ||
"field": "field_name", | ||
"index_filter": Object { | ||
"bool": Object { | ||
"must": Array [ | ||
Object { | ||
"terms": Object { | ||
"_tier": Array [ | ||
"data_hot", | ||
"data_warm", | ||
"data_content", | ||
], | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
"string": "query", | ||
}, | ||
"method": "POST", | ||
"path": "/index/_terms_enum", | ||
} | ||
`); | ||
expect(result).toEqual(mockResponse.body.terms); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* | ||
* 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 { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; | ||
import { estypes } from '@elastic/elasticsearch'; | ||
import { IFieldType } from '../../common'; | ||
import { findIndexPatternById, getFieldByName } from '../index_patterns'; | ||
import { shimAbortSignal } from '../search'; | ||
import { getKbnServerError } from '../../../kibana_utils/server'; | ||
import { ConfigSchema } from '../../config'; | ||
|
||
export async function termsEnumSuggestions( | ||
config: ConfigSchema, | ||
savedObjectsClient: SavedObjectsClientContract, | ||
esClient: ElasticsearchClient, | ||
index: string, | ||
fieldName: string, | ||
query: string, | ||
filters?: estypes.QueryDslQueryContainer[], | ||
field?: IFieldType, | ||
abortSignal?: AbortSignal | ||
) { | ||
const { tiers } = config.autocomplete.valueSuggestions; | ||
if (!field?.name && !field?.type) { | ||
const indexPattern = await findIndexPatternById(savedObjectsClient, index); | ||
field = indexPattern && getFieldByName(fieldName, indexPattern); | ||
} | ||
|
||
try { | ||
const promise = esClient.transport.request({ | ||
method: 'POST', | ||
path: encodeURI(`/${index}/_terms_enum`), | ||
body: { | ||
field: field?.name ?? field, | ||
string: query, | ||
index_filter: { | ||
bool: { | ||
must: [ | ||
...(filters ?? []), | ||
{ | ||
terms: { | ||
_tier: tiers, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const result = await shimAbortSignal(promise, abortSignal); | ||
|
||
return result.body.terms; | ||
} catch (e) { | ||
throw getKbnServerError(e); | ||
} | ||
} |
Oops, something went wrong.