[8.7] [Controls] Fix sorting of numeric keyword fields (#155207) (#155934)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Controls] Fix sorting of numeric keyword fields
(#155207)](https://github.com/elastic/kibana/pull/155207)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Hannah
Mudge","email":"Heenawter@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-04-26T18:19:46Z","message":"[Controls]
Fix sorting of numeric keyword fields (#155207)\n\nCloses
https://github.com/elastic/kibana/issues/155073\r\n\r\n##
Summary\r\n\r\n### Before\r\n\r\nPreviously, the options list
suggestions were stored as a dictionary\r\n(i.e. an object of key+value
pairs) - while this worked for most fields,\r\nunbeknownst to us,
Javascript tries to sort numeric keys (regardless of\r\nif they are of
type `string` or `number`) based on their value.\r\n\r\nThis meant that,
as part of the parsing process when using an options\r\nlist control for
a numeric `keyword` field, the results returned by the\r\nES query were
**always** sorted in ascending numeric order regardless of\r\nthe
sorting method that was picked (note that this is especially
obvious\r\nonce you \"load more\", which is what I did for the
following\r\nscreenshots):\r\n\r\n\r\n| | Ascending | Descending
|\r\n|--------------|-----------|------------|\r\n| Alphabetical | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391308-6d3a23ee-3495-4eff-810f-216f758b3a58.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391213-117163e2-ee97-4f9d-87fa-a63c8cc5459e.png\"/>\r\n|\r\n|
Doc count | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391375-0ccdf72f-83c0-4a87-951e-c2e1e3223006.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234392997-fea42ffe-5d9d-4a11-968f-e1503f2c0e4f.png\"/>\r\n|\r\n\r\n\r\n###
After\r\n\r\nThis PR converts the options list suggestions to be stored
as an\r\n**array** of key/value pairs in order to preserve the order
returned\r\nfrom Elasticsearch - now, you get the expected string-sorted
ordering\r\nwhen using numeric `keyword` fields in an options list
control:\r\n\r\n| | Ascending | Descending
|\r\n|--------------|-----------|------------|\r\n| Alphabetical | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394182-aa2bfdf4-fe41-441d-bdbf-917173c17627.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234393421-24cca3e3-0249-4607-9e16-daa274399bdd.png\"/>\r\n|\r\n|
Doc count | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394599-dda01056-5446-497e-abe4-f3839aeb4dd0.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394693-42544ef1-eb2b-4d52-8a78-c2ca7d2d7cfa.png\"/>\r\n|\r\n\r\n\r\nNotice
in the above that we are now using **string sorting** for the\r\nnumeric
values when alphabetical sorting is selected, which means you\r\naren't
getting the expected \"numeric\" sorting - so for example,
when\r\nsorted ascending, `\"6\" > \"52\"` because it is only comparing
the first\r\ncharacter and `\"6\" > \"5\"`. This will be handled much
better once\r\n[numeric field
support](https://github.com/elastic/kibana/issues/126795)\r\nis added to
options lists.\r\n\r\n\r\n### Checklist\r\n\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"b3f65f79e5017b70fe26e5aa1c2ee1085e68c138","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Feature:Input
Control","Team:Presentation","loe:days","impact:medium","backport:prev-minor","v8.8.0","v8.9.0"],"number":155207,"url":"https://github.com/elastic/kibana/pull/155207","mergeCommit":{"message":"[Controls]
Fix sorting of numeric keyword fields (#155207)\n\nCloses
https://github.com/elastic/kibana/issues/155073\r\n\r\n##
Summary\r\n\r\n### Before\r\n\r\nPreviously, the options list
suggestions were stored as a dictionary\r\n(i.e. an object of key+value
pairs) - while this worked for most fields,\r\nunbeknownst to us,
Javascript tries to sort numeric keys (regardless of\r\nif they are of
type `string` or `number`) based on their value.\r\n\r\nThis meant that,
as part of the parsing process when using an options\r\nlist control for
a numeric `keyword` field, the results returned by the\r\nES query were
**always** sorted in ascending numeric order regardless of\r\nthe
sorting method that was picked (note that this is especially
obvious\r\nonce you \"load more\", which is what I did for the
following\r\nscreenshots):\r\n\r\n\r\n| | Ascending | Descending
|\r\n|--------------|-----------|------------|\r\n| Alphabetical | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391308-6d3a23ee-3495-4eff-810f-216f758b3a58.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391213-117163e2-ee97-4f9d-87fa-a63c8cc5459e.png\"/>\r\n|\r\n|
Doc count | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391375-0ccdf72f-83c0-4a87-951e-c2e1e3223006.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234392997-fea42ffe-5d9d-4a11-968f-e1503f2c0e4f.png\"/>\r\n|\r\n\r\n\r\n###
After\r\n\r\nThis PR converts the options list suggestions to be stored
as an\r\n**array** of key/value pairs in order to preserve the order
returned\r\nfrom Elasticsearch - now, you get the expected string-sorted
ordering\r\nwhen using numeric `keyword` fields in an options list
control:\r\n\r\n| | Ascending | Descending
|\r\n|--------------|-----------|------------|\r\n| Alphabetical | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394182-aa2bfdf4-fe41-441d-bdbf-917173c17627.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234393421-24cca3e3-0249-4607-9e16-daa274399bdd.png\"/>\r\n|\r\n|
Doc count | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394599-dda01056-5446-497e-abe4-f3839aeb4dd0.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394693-42544ef1-eb2b-4d52-8a78-c2ca7d2d7cfa.png\"/>\r\n|\r\n\r\n\r\nNotice
in the above that we are now using **string sorting** for the\r\nnumeric
values when alphabetical sorting is selected, which means you\r\naren't
getting the expected \"numeric\" sorting - so for example,
when\r\nsorted ascending, `\"6\" > \"52\"` because it is only comparing
the first\r\ncharacter and `\"6\" > \"5\"`. This will be handled much
better once\r\n[numeric field
support](https://github.com/elastic/kibana/issues/126795)\r\nis added to
options lists.\r\n\r\n\r\n### Checklist\r\n\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"b3f65f79e5017b70fe26e5aa1c2ee1085e68c138"}},"sourceBranch":"main","suggestedTargetBranches":["8.9"],"targetPullRequestStates":[{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/155207","number":155207,"mergeCommit":{"message":"[Controls]
Fix sorting of numeric keyword fields (#155207)\n\nCloses
https://github.com/elastic/kibana/issues/155073\r\n\r\n##
Summary\r\n\r\n### Before\r\n\r\nPreviously, the options list
suggestions were stored as a dictionary\r\n(i.e. an object of key+value
pairs) - while this worked for most fields,\r\nunbeknownst to us,
Javascript tries to sort numeric keys (regardless of\r\nif they are of
type `string` or `number`) based on their value.\r\n\r\nThis meant that,
as part of the parsing process when using an options\r\nlist control for
a numeric `keyword` field, the results returned by the\r\nES query were
**always** sorted in ascending numeric order regardless of\r\nthe
sorting method that was picked (note that this is especially
obvious\r\nonce you \"load more\", which is what I did for the
following\r\nscreenshots):\r\n\r\n\r\n| | Ascending | Descending
|\r\n|--------------|-----------|------------|\r\n| Alphabetical | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391308-6d3a23ee-3495-4eff-810f-216f758b3a58.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391213-117163e2-ee97-4f9d-87fa-a63c8cc5459e.png\"/>\r\n|\r\n|
Doc count | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234391375-0ccdf72f-83c0-4a87-951e-c2e1e3223006.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234392997-fea42ffe-5d9d-4a11-968f-e1503f2c0e4f.png\"/>\r\n|\r\n\r\n\r\n###
After\r\n\r\nThis PR converts the options list suggestions to be stored
as an\r\n**array** of key/value pairs in order to preserve the order
returned\r\nfrom Elasticsearch - now, you get the expected string-sorted
ordering\r\nwhen using numeric `keyword` fields in an options list
control:\r\n\r\n| | Ascending | Descending
|\r\n|--------------|-----------|------------|\r\n| Alphabetical | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394182-aa2bfdf4-fe41-441d-bdbf-917173c17627.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234393421-24cca3e3-0249-4607-9e16-daa274399bdd.png\"/>\r\n|\r\n|
Doc count | <img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394599-dda01056-5446-497e-abe4-f3839aeb4dd0.png\"/>\r\n|
<img
width=\"320px\"\r\nsrc=\"https://user-images.githubusercontent.com/8698078/234394693-42544ef1-eb2b-4d52-8a78-c2ca7d2d7cfa.png\"/>\r\n|\r\n\r\n\r\nNotice
in the above that we are now using **string sorting** for the\r\nnumeric
values when alphabetical sorting is selected, which means you\r\naren't
getting the expected \"numeric\" sorting - so for example,
when\r\nsorted ascending, `\"6\" > \"52\"` because it is only comparing
the first\r\ncharacter and `\"6\" > \"5\"`. This will be handled much
better once\r\n[numeric field
support](https://github.com/elastic/kibana/issues/126795)\r\nis added to
options lists.\r\n\r\n\r\n### Checklist\r\n\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"b3f65f79e5017b70fe26e5aa1c2ee1085e68c138"}},{"branch":"8.9","label":"v8.9.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Hannah Mudge 2023-04-26 15:06:03 -06:00 committed by GitHub
parent 5d80eb581c
commit 167c3285d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 196 additions and 191 deletions

View file

@ -21,13 +21,13 @@ const mockOptionsListComponentState = {
...getDefaultComponentState(),
field: undefined,
totalCardinality: 0,
availableOptions: {
woof: { doc_count: 100 },
bark: { doc_count: 75 },
meow: { doc_count: 50 },
quack: { doc_count: 25 },
moo: { doc_count: 5 },
},
availableOptions: [
{ value: 'woof', docCount: 100 },
{ value: 'bark', docCount: 75 },
{ value: 'meow', docCount: 50 },
{ value: 'quack', docCount: 25 },
{ value: 'moo', docCount: 5 },
],
invalidSelections: [],
validSelections: [],
} as OptionsListComponentState;

View file

@ -28,9 +28,7 @@ export interface OptionsListEmbeddableInput extends DataControlInput {
placeholder?: string;
}
export interface OptionsListSuggestions {
[key: string]: { doc_count: number };
}
export type OptionsListSuggestions = Array<{ value: string; docCount?: number }>;
/**
* The Options list response is returned from the serverside Options List route.

View file

@ -35,7 +35,11 @@ import { injectStorybookDataView } from '../services/data_views/data_views.story
import { replaceOptionsListMethod } from '../services/options_list/options_list.story';
import { populateStorybookControlFactories } from './storybook_control_factories';
import { replaceValueSuggestionMethod } from '../services/unified_search/unified_search.story';
import { OptionsListResponse, OptionsListRequest } from '../../common/options_list/types';
import {
OptionsListResponse,
OptionsListRequest,
OptionsListSuggestions,
} from '../../common/options_list/types';
export default {
title: 'Controls',
@ -56,9 +60,9 @@ const storybookStubOptionsListRequest = async (
r({
suggestions: getFlightSearchOptions(request.field.name, request.searchString).reduce(
(o, current, index) => {
return { ...o, [current]: { doc_count: index } };
return [...o, { value: current, docCount: index }];
},
{}
[] as OptionsListSuggestions
),
totalCardinality: 100,
}),

View file

@ -68,7 +68,7 @@ describe('Options list popover', () => {
});
test('no available options', async () => {
const popover = await mountComponent({ componentState: { availableOptions: {} } });
const popover = await mountComponent({ componentState: { availableOptions: [] } });
const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
const noOptionsDiv = findTestSubject(
availableOptionsDiv,
@ -125,9 +125,7 @@ describe('Options list popover', () => {
selectedOptions: ['bark', 'woof'],
},
componentState: {
availableOptions: {
bark: { doc_count: 75 },
},
availableOptions: [{ value: 'bark', docCount: 75 }],
validSelections: ['bark'],
invalidSelections: ['woof'],
},
@ -152,9 +150,7 @@ describe('Options list popover', () => {
const popover = await mountComponent({
explicitInput: { selectedOptions: ['bark', 'woof', 'meow'] },
componentState: {
availableOptions: {
bark: { doc_count: 75 },
},
availableOptions: [{ value: 'bark', docCount: 75 }],
validSelections: ['bark'],
invalidSelections: ['woof', 'meow'],
},
@ -217,7 +213,7 @@ describe('Options list popover', () => {
test('if existsSelected = false and no suggestions, then "Exists" does not show up', async () => {
const popover = await mountComponent({
componentState: { availableOptions: {} },
componentState: { availableOptions: [] },
explicitInput: { existsSelected: false },
});
const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists');

View file

@ -55,7 +55,7 @@ export const OptionsListPopoverSuggestions = ({
const canLoadMoreSuggestions = useMemo(
() =>
totalCardinality
? Object.keys(availableOptions ?? {}).length <
? (availableOptions ?? []).length <
Math.min(totalCardinality, MAX_OPTIONS_LIST_REQUEST_SIZE)
: false,
[availableOptions, totalCardinality]
@ -68,7 +68,7 @@ export const OptionsListPopoverSuggestions = ({
[invalidSelections]
);
const suggestions = useMemo(() => {
return showOnlySelected ? selectedOptions : Object.keys(availableOptions ?? {});
return showOnlySelected ? selectedOptions : availableOptions ?? [];
}, [availableOptions, selectedOptions, showOnlySelected]);
const existsSelectableOption = useMemo<EuiSelectableOption | undefined>(() => {
@ -86,19 +86,23 @@ export const OptionsListPopoverSuggestions = ({
const [selectableOptions, setSelectableOptions] = useState<EuiSelectableOption[]>([]); // will be set in following useEffect
useEffect(() => {
/* This useEffect makes selectableOptions responsive to search, show only selected, and clear selections */
const options: EuiSelectableOption[] = (suggestions ?? []).map((key) => {
const options: EuiSelectableOption[] = (suggestions ?? []).map((suggestion) => {
if (typeof suggestion === 'string') {
// this means that `showOnlySelected` is true, and doc count is not known when this is the case
suggestion = { value: suggestion };
}
return {
key,
label: key,
checked: selectedOptionsSet?.has(key) ? 'on' : undefined,
'data-test-subj': `optionsList-control-selection-${key}`,
key: suggestion.value,
label: suggestion.value,
checked: selectedOptionsSet?.has(suggestion.value) ? 'on' : undefined,
'data-test-subj': `optionsList-control-selection-${suggestion.value}`,
className:
showOnlySelected && invalidSelectionsSet.has(key)
showOnlySelected && invalidSelectionsSet.has(suggestion.value)
? 'optionsList__selectionInvalid'
: 'optionsList__validSuggestion',
append:
!showOnlySelected && availableOptions?.[key] ? (
<OptionsListPopoverSuggestionBadge documentCount={availableOptions[key].doc_count} />
!showOnlySelected && suggestion?.docCount ? (
<OptionsListPopoverSuggestionBadge documentCount={suggestion.docCount} />
) : undefined,
};
});

View file

@ -377,7 +377,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
batch(() => {
dispatch(
updateQueryResults({
availableOptions: {},
availableOptions: [],
})
);
dispatch(setLoading(false));

View file

@ -18,7 +18,7 @@ let optionsListRequestMethod = async (request: OptionsListRequest, abortSignal:
setTimeout(
() =>
r({
suggestions: {},
suggestions: [],
totalCardinality: 100,
}),
120

View file

@ -388,17 +388,20 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
Object {
"cool1": Object {
"doc_count": 5,
Array [
Object {
"docCount": 5,
"value": "cool1",
},
"cool2": Object {
"doc_count": 15,
Object {
"docCount": 15,
"value": "cool2",
},
"cool3": Object {
"doc_count": 10,
Object {
"docCount": 10,
"value": "cool3",
},
}
]
`);
});
@ -421,14 +424,16 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
Object {
"false": Object {
"doc_count": 55,
Array [
Object {
"docCount": 55,
"value": "false",
},
"true": Object {
"doc_count": 155,
Object {
"docCount": 155,
"value": "true",
},
}
]
`);
});
@ -455,17 +460,20 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
Object {
"cool1": Object {
"doc_count": 5,
Array [
Object {
"docCount": 5,
"value": "cool1",
},
"cool2": Object {
"doc_count": 15,
Object {
"docCount": 15,
"value": "cool2",
},
"cool3": Object {
"doc_count": 10,
Object {
"docCount": 10,
"value": "cool3",
},
}
]
`);
});
@ -490,17 +498,20 @@ describe('options list cheap queries', () => {
expect(
suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock).suggestions
).toMatchInlineSnapshot(`
Object {
"cool1": Object {
"doc_count": 5,
Array [
Object {
"docCount": 5,
"value": "cool1",
},
"cool2": Object {
"doc_count": 15,
Object {
"docCount": 15,
"value": "cool2",
},
"cool3": Object {
"doc_count": 10,
Object {
"docCount": 10,
"value": "cool3",
},
}
]
`);
});
});
@ -552,55 +563,50 @@ describe('options list cheap queries', () => {
rawSearchResponseMock,
optionsListRequestBodyMock
).suggestions;
/** first, verify that the sorting worked as expected */
expect(Object.keys(parsed)).toMatchInlineSnapshot(`
Array [
"52:ae76:5947:5e2a:551:fe6a:712a:c72",
"111.52.174.2",
"196.162.13.39",
"f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
"23.216.241.120",
"28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
"21.35.91.62",
"21.35.91.61",
"203.88.33.151",
"1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
]
`);
/** then, make sure the object is structured properly */
expect(parsed).toMatchInlineSnapshot(`
Object {
"111.52.174.2": Object {
"doc_count": 11,
Array [
Object {
"docCount": 12,
"value": "52:ae76:5947:5e2a:551:fe6a:712a:c72",
},
"196.162.13.39": Object {
"doc_count": 10,
Object {
"docCount": 11,
"value": "111.52.174.2",
},
"1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8": Object {
"doc_count": 6,
Object {
"docCount": 10,
"value": "196.162.13.39",
},
"203.88.33.151": Object {
"doc_count": 7,
Object {
"docCount": 10,
"value": "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
},
"21.35.91.61": Object {
"doc_count": 8,
Object {
"docCount": 9,
"value": "23.216.241.120",
},
"21.35.91.62": Object {
"doc_count": 8,
Object {
"docCount": 9,
"value": "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
},
"23.216.241.120": Object {
"doc_count": 9,
Object {
"docCount": 8,
"value": "21.35.91.62",
},
"28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172": Object {
"doc_count": 9,
Object {
"docCount": 8,
"value": "21.35.91.61",
},
"52:ae76:5947:5e2a:551:fe6a:712a:c72": Object {
"doc_count": 12,
Object {
"docCount": 7,
"value": "203.88.33.151",
},
"f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63": Object {
"doc_count": 10,
Object {
"docCount": 6,
"value": "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
},
}
]
`);
});
});

View file

@ -51,11 +51,11 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
},
}),
parse: (rawEsResult) => ({
suggestions: get(rawEsResult, 'aggregations.suggestions.buckets').reduce(
(suggestions: OptionsListSuggestions, suggestion: EsBucket) => {
return { ...suggestions, [suggestion.key]: { doc_count: suggestion.doc_count } };
suggestions: get(rawEsResult, 'aggregations.suggestions.buckets')?.reduce(
(acc: OptionsListSuggestions, suggestion: EsBucket) => {
return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
},
{}
[]
),
}),
},
@ -75,13 +75,10 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
}),
parse: (rawEsResult) => ({
suggestions: get(rawEsResult, 'aggregations.suggestions.buckets')?.reduce(
(suggestions: OptionsListSuggestions, suggestion: EsBucket & { key_as_string: string }) => {
return {
...suggestions,
[suggestion.key_as_string]: { doc_count: suggestion.doc_count },
};
(acc: OptionsListSuggestions, suggestion: EsBucket & { key_as_string: string }) => {
return [...acc, { value: suggestion.key_as_string, docCount: suggestion.doc_count }];
},
{}
[]
),
}),
},
@ -134,7 +131,7 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
if (!Boolean(rawEsResult.aggregations?.suggestions)) {
// if this is happens, that means there is an invalid search that snuck through to the server side code;
// so, might as well early return with no suggestions
return { suggestions: {} };
return { suggestions: [] };
}
const buckets: EsBucket[] = [];
@ -153,9 +150,9 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
return {
suggestions: sortedSuggestions
.slice(0, 10) // only return top 10 results
.reduce((suggestions, suggestion: EsBucket) => {
return { ...suggestions, [suggestion.key]: { doc_count: suggestion.doc_count } };
}, {}),
.reduce((acc: OptionsListSuggestions, suggestion: EsBucket) => {
return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
}, []),
};
},
},
@ -190,11 +187,11 @@ const cheapSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregat
};
},
parse: (rawEsResult) => ({
suggestions: get(rawEsResult, 'aggregations.nestedSuggestions.suggestions.buckets').reduce(
(suggestions: OptionsListSuggestions, suggestion: EsBucket) => {
return { ...suggestions, [suggestion.key]: { doc_count: suggestion.doc_count } };
suggestions: get(rawEsResult, 'aggregations.nestedSuggestions.suggestions.buckets')?.reduce(
(acc: OptionsListSuggestions, suggestion: EsBucket) => {
return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
},
{}
[]
),
}),
},

View file

@ -466,17 +466,20 @@ describe('options list expensive queries', () => {
expect(suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock))
.toMatchInlineSnapshot(`
Object {
"suggestions": Object {
"cool1": Object {
"doc_count": 5,
"suggestions": Array [
Object {
"docCount": 5,
"value": "cool1",
},
"cool2": Object {
"doc_count": 15,
Object {
"docCount": 15,
"value": "cool2",
},
"cool3": Object {
"doc_count": 10,
Object {
"docCount": 10,
"value": "cool3",
},
},
],
"totalCardinality": 3,
}
`);
@ -503,14 +506,16 @@ describe('options list expensive queries', () => {
expect(suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock))
.toMatchInlineSnapshot(`
Object {
"suggestions": Object {
"false": Object {
"doc_count": 55,
"suggestions": Array [
Object {
"docCount": 55,
"value": "false",
},
"true": Object {
"doc_count": 155,
Object {
"docCount": 155,
"value": "true",
},
},
],
"totalCardinality": 2,
}
`);
@ -546,17 +551,20 @@ describe('options list expensive queries', () => {
expect(suggestionAggBuilder.parse(rawSearchResponseMock, optionsListRequestBodyMock))
.toMatchInlineSnapshot(`
Object {
"suggestions": Object {
"cool1": Object {
"doc_count": 5,
"suggestions": Array [
Object {
"docCount": 5,
"value": "cool1",
},
"cool2": Object {
"doc_count": 15,
Object {
"docCount": 15,
"value": "cool2",
},
"cool3": Object {
"doc_count": 10,
Object {
"docCount": 10,
"value": "cool3",
},
},
],
"totalCardinality": 3,
}
`);
@ -621,55 +629,50 @@ describe('options list expensive queries', () => {
rawSearchResponseMock,
optionsListRequestBodyMock
).suggestions;
/** first, verify that the sorting worked as expected */
expect(Object.keys(parsed)).toMatchInlineSnapshot(`
Array [
"52:ae76:5947:5e2a:551:fe6a:712a:c72",
"111.52.174.2",
"196.162.13.39",
"f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
"23.216.241.120",
"28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
"21.35.91.62",
"21.35.91.61",
"203.88.33.151",
"1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
]
`);
/** then, make sure the object is structured properly */
expect(parsed).toMatchInlineSnapshot(`
Object {
"111.52.174.2": Object {
"doc_count": 11,
Array [
Object {
"docCount": 12,
"value": "52:ae76:5947:5e2a:551:fe6a:712a:c72",
},
"196.162.13.39": Object {
"doc_count": 10,
Object {
"docCount": 11,
"value": "111.52.174.2",
},
"1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8": Object {
"doc_count": 6,
Object {
"docCount": 10,
"value": "196.162.13.39",
},
"203.88.33.151": Object {
"doc_count": 7,
Object {
"docCount": 10,
"value": "f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63",
},
"21.35.91.61": Object {
"doc_count": 8,
Object {
"docCount": 9,
"value": "23.216.241.120",
},
"21.35.91.62": Object {
"doc_count": 8,
Object {
"docCount": 9,
"value": "28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172",
},
"23.216.241.120": Object {
"doc_count": 9,
Object {
"docCount": 8,
"value": "21.35.91.62",
},
"28c7:c9a4:42fd:16b0:4de5:e41e:28d9:9172": Object {
"doc_count": 9,
Object {
"docCount": 8,
"value": "21.35.91.61",
},
"52:ae76:5947:5e2a:551:fe6a:712a:c72": Object {
"doc_count": 12,
Object {
"docCount": 7,
"value": "203.88.33.151",
},
"f7a9:640b:b5a0:1219:8d75:ed94:3c3e:2e63": Object {
"doc_count": 10,
Object {
"docCount": 6,
"value": "1ec:aa98:b0a6:d07c:590:18a0:8a33:2eb8",
},
}
]
`);
});
});

View file

@ -93,9 +93,9 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
const suggestions = get(rawEsResult, `${basePath}.suggestions.buckets`)?.reduce(
(acc: OptionsListSuggestions, suggestion: EsBucket) => {
return { ...acc, [suggestion.key]: { doc_count: suggestion.doc_count } };
return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
},
{}
[]
);
return {
suggestions,
@ -120,14 +120,11 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
parse: (rawEsResult) => {
const suggestions = get(rawEsResult, 'aggregations.suggestions.buckets')?.reduce(
(acc: OptionsListSuggestions, suggestion: EsBucket & { key_as_string: string }) => {
return {
...acc,
[suggestion.key_as_string]: { doc_count: suggestion.doc_count },
};
return [...acc, { value: suggestion.key_as_string, docCount: suggestion.doc_count }];
},
{}
[]
);
return { suggestions, totalCardinality: Object.keys(suggestions).length }; // cardinality is only ever 0, 1, or 2 so safe to use length here
return { suggestions, totalCardinality: suggestions.length }; // cardinality is only ever 0, 1, or 2 so safe to use length here
},
},
@ -185,7 +182,7 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
if (!Boolean(rawEsResult.aggregations?.suggestions)) {
// if this is happens, that means there is an invalid search that snuck through to the server side code;
// so, might as well early return with no suggestions
return { suggestions: {}, totalCardinality: 0 };
return { suggestions: [], totalCardinality: 0 };
}
const buckets: EsBucket[] = [];
getIpBuckets(rawEsResult, buckets, 'ipv4'); // modifies buckets array directly, i.e. "by reference"
@ -200,11 +197,11 @@ const expensiveSuggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggr
(bucketA: EsBucket, bucketB: EsBucket) => bucketB.doc_count - bucketA.doc_count
);
const suggestions: OptionsListSuggestions = sortedSuggestions
const suggestions = sortedSuggestions
.slice(0, request.size)
.reduce((acc: OptionsListSuggestions, suggestion: EsBucket) => {
return { ...acc, [suggestion.key]: { doc_count: suggestion.doc_count } };
}, {});
return [...acc, { value: suggestion.key, docCount: suggestion.doc_count }];
}, []);
const totalCardinality =
(get(rawEsResult, `aggregations.suggestions.buckets.ipv4.unique_terms.value`) ?? 0) +
(get(rawEsResult, `aggregations.suggestions.buckets.ipv6.unique_terms.value`) ?? 0);