Skip to content

Commit b75aac6

Browse files
[8.x] [Global Search] Add multiword type handling in global search (#196087) (#196545)
# Backport This will backport the following commits from `main` to `8.x`: - [[Global Search] Add multiword type handling in global search (#196087)](#196087) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Krzysztof Kowalczyk","email":"krzysztof.kowalczyk@elastic.co"},"sourceCommit":{"committedDate":"2024-10-16T12:29:03Z","message":"[Global Search] Add multiword type handling in global search (#196087)\n\n## Summary\r\n\r\nThis PR improves the UX of global search by allowing users to search for\r\ntypes that consist of multiple words without having to turn them into\r\nphrases (wrapping them in quotes).\r\n\r\nFor example: \r\n\r\nThe following query:\r\n```\r\nhello type:canvas workpad type:enterprise search world tag:new\r\n```\r\nWill get mapped to:\r\n```\r\nhello type:\"canvas workpad\" type:\"enterprise search\" world tag:new\r\n```\r\nWhich will result in following `Query` object:\r\n```json\r\n{\r\n \"term\": \"hello world\",\r\n \"filters\": {\r\n \"tags\": [\"new\"]\r\n \"types\": [\"canvas workpad\", \"enterprise search\"],\r\n },\r\n}\r\n```\r\n\r\nFixes: #176877\r\n\r\n### Checklist\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","sha":"3d28d173a94dc9856fe43cbff8d88ac4e2d42a17","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","Team:SharedUX","backport:prev-minor"],"title":"[Global Search] Add multiword type handling in global search","number":196087,"url":"https://github.com/elastic/kibana/pull/196087","mergeCommit":{"message":"[Global Search] Add multiword type handling in global search (#196087)\n\n## Summary\r\n\r\nThis PR improves the UX of global search by allowing users to search for\r\ntypes that consist of multiple words without having to turn them into\r\nphrases (wrapping them in quotes).\r\n\r\nFor example: \r\n\r\nThe following query:\r\n```\r\nhello type:canvas workpad type:enterprise search world tag:new\r\n```\r\nWill get mapped to:\r\n```\r\nhello type:\"canvas workpad\" type:\"enterprise search\" world tag:new\r\n```\r\nWhich will result in following `Query` object:\r\n```json\r\n{\r\n \"term\": \"hello world\",\r\n \"filters\": {\r\n \"tags\": [\"new\"]\r\n \"types\": [\"canvas workpad\", \"enterprise search\"],\r\n },\r\n}\r\n```\r\n\r\nFixes: #176877\r\n\r\n### Checklist\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","sha":"3d28d173a94dc9856fe43cbff8d88ac4e2d42a17"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196087","number":196087,"mergeCommit":{"message":"[Global Search] Add multiword type handling in global search (#196087)\n\n## Summary\r\n\r\nThis PR improves the UX of global search by allowing users to search for\r\ntypes that consist of multiple words without having to turn them into\r\nphrases (wrapping them in quotes).\r\n\r\nFor example: \r\n\r\nThe following query:\r\n```\r\nhello type:canvas workpad type:enterprise search world tag:new\r\n```\r\nWill get mapped to:\r\n```\r\nhello type:\"canvas workpad\" type:\"enterprise search\" world tag:new\r\n```\r\nWhich will result in following `Query` object:\r\n```json\r\n{\r\n \"term\": \"hello world\",\r\n \"filters\": {\r\n \"tags\": [\"new\"]\r\n \"types\": [\"canvas workpad\", \"enterprise search\"],\r\n },\r\n}\r\n```\r\n\r\nFixes: #176877\r\n\r\n### Checklist\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","sha":"3d28d173a94dc9856fe43cbff8d88ac4e2d42a17"}}]}] BACKPORT--> Co-authored-by: Krzysztof Kowalczyk <krzysztof.kowalczyk@elastic.co>
1 parent 74b1ca6 commit b75aac6

3 files changed

Lines changed: 124 additions & 12 deletions

File tree

x-pack/plugins/global_search_bar/public/components/search_bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export const SearchBar: FC<SearchBarProps> = (opts) => {
173173
reportEvent.searchRequest();
174174
}
175175

176-
const rawParams = parseSearchParams(searchValue.toLowerCase());
176+
const rawParams = parseSearchParams(searchValue.toLowerCase(), searchableTypes);
177177
let tagIds: string[] | undefined;
178178
if (taggingApi && rawParams.filters.tags) {
179179
tagIds = rawParams.filters.tags.map(

x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,33 @@ import { parseSearchParams } from './parse_search_params';
99

1010
describe('parseSearchParams', () => {
1111
it('returns the correct term', () => {
12-
const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello');
12+
const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello', []);
1313
expect(searchParams.term).toEqual('hello');
1414
});
1515

1616
it('returns the raw query as `term` in case of parsing error', () => {
17-
const searchParams = parseSearchParams('tag:((()^invalid');
17+
const searchParams = parseSearchParams('tag:((()^invalid', []);
1818
expect(searchParams).toEqual({
1919
term: 'tag:((()^invalid',
2020
filters: {},
2121
});
2222
});
2323

2424
it('returns `undefined` term if query only contains field clauses', () => {
25-
const searchParams = parseSearchParams('tag:(my-tag OR other-tag)');
25+
const searchParams = parseSearchParams('tag:(my-tag OR other-tag)', []);
2626
expect(searchParams.term).toBeUndefined();
2727
});
2828

2929
it('returns correct filters when no field clause is defined', () => {
30-
const searchParams = parseSearchParams('hello');
30+
const searchParams = parseSearchParams('hello', []);
3131
expect(searchParams.filters).toEqual({
3232
tags: undefined,
3333
types: undefined,
3434
});
3535
});
3636

3737
it('returns correct filters when field clauses are present', () => {
38-
const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly');
38+
const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly', []);
3939
expect(searchParams).toEqual({
4040
term: 'hello',
4141
filters: {
@@ -46,7 +46,7 @@ describe('parseSearchParams', () => {
4646
});
4747

4848
it('considers unknown field clauses to be part of the raw search term', () => {
49-
const searchParams = parseSearchParams('tag:foo unknown:bar hello');
49+
const searchParams = parseSearchParams('tag:foo unknown:bar hello', []);
5050
expect(searchParams).toEqual({
5151
term: 'unknown:bar hello',
5252
filters: {
@@ -56,7 +56,7 @@ describe('parseSearchParams', () => {
5656
});
5757

5858
it('handles aliases field clauses', () => {
59-
const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello');
59+
const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello', []);
6060
expect(searchParams).toEqual({
6161
term: 'hello',
6262
filters: {
@@ -67,7 +67,7 @@ describe('parseSearchParams', () => {
6767
});
6868

6969
it('converts boolean and number values to string for known filters', () => {
70-
const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello');
70+
const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello', []);
7171
expect(searchParams).toEqual({
7272
term: 'hello',
7373
filters: {
@@ -76,4 +76,74 @@ describe('parseSearchParams', () => {
7676
},
7777
});
7878
});
79+
80+
it('converts multiword searchable types to phrases so they get picked up as types', () => {
81+
const mockSearchableMultiwordTypes = ['canvas-workpad', 'enterprise search'];
82+
const searchParams = parseSearchParams(
83+
'type:canvas workpad types:canvas-workpad hello type:enterprise search type:not multiword',
84+
mockSearchableMultiwordTypes
85+
);
86+
expect(searchParams).toEqual({
87+
term: 'hello multiword',
88+
filters: {
89+
types: ['canvas workpad', 'enterprise search', 'not'],
90+
},
91+
});
92+
});
93+
94+
it('parses correctly when multiword types are already quoted', () => {
95+
const mockSearchableMultiwordTypes = ['canvas-workpad'];
96+
const searchParams = parseSearchParams(
97+
`type:"canvas workpad" hello type:"dashboard"`,
98+
mockSearchableMultiwordTypes
99+
);
100+
expect(searchParams).toEqual({
101+
term: 'hello',
102+
filters: {
103+
types: ['canvas workpad', 'dashboard'],
104+
},
105+
});
106+
});
107+
108+
it('parses correctly when there is whitespace between type keyword and value', () => {
109+
const mockSearchableMultiwordTypes = ['canvas-workpad'];
110+
const searchParams = parseSearchParams(
111+
'type: canvas workpad hello type: dashboard',
112+
mockSearchableMultiwordTypes
113+
);
114+
expect(searchParams).toEqual({
115+
term: 'hello',
116+
filters: {
117+
types: ['canvas workpad', 'dashboard'],
118+
},
119+
});
120+
});
121+
122+
it('dedupes duplicate types', () => {
123+
const mockSearchableMultiwordTypes = ['canvas-workpad'];
124+
const searchParams = parseSearchParams(
125+
'type:canvas workpad hello type:dashboard type:canvas-workpad type:canvas workpad type:dashboard',
126+
mockSearchableMultiwordTypes
127+
);
128+
expect(searchParams).toEqual({
129+
term: 'hello',
130+
filters: {
131+
types: ['canvas workpad', 'dashboard'],
132+
},
133+
});
134+
});
135+
136+
it('handles whitespace removal even if there are no multiword types', () => {
137+
const mockSearchableMultiwordTypes: string[] = [];
138+
const searchParams = parseSearchParams(
139+
'hello type: dashboard',
140+
mockSearchableMultiwordTypes
141+
);
142+
expect(searchParams).toEqual({
143+
term: 'hello',
144+
filters: {
145+
types: ['dashboard'],
146+
},
147+
});
148+
});
79149
});

x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,54 @@ const aliasMap = {
1616
type: ['types'],
1717
};
1818

19-
export const parseSearchParams = (term: string): ParsedSearchParams => {
19+
// Converts multiword types to phrases by wrapping them in quotes and trimming whitespace after type keyword. Example: type: canvas workpad -> type:"canvas workpad". If the type is already wrapped in quotes or is a single word, it will only trim whitespace after type keyword.
20+
const convertMultiwordTypesToPhrasesAndTrimWhitespace = (
21+
term: string,
22+
multiWordTypes: string[]
23+
): string => {
24+
if (!multiWordTypes.length) {
25+
return term.replace(
26+
/(type:|types:)\s*([^"']*?)\b([^"'\s]+)/gi,
27+
(_, typeKeyword, whitespace, typeValue) => `${typeKeyword}${whitespace.trim()}${typeValue}`
28+
);
29+
}
30+
31+
const typesPattern = multiWordTypes.join('|');
32+
const termReplaceRegex = new RegExp(
33+
`(type:|types:)\\s*([^"']*?)\\b((${typesPattern})\\b|[^\\s"']+)`,
34+
'gi'
35+
);
36+
37+
return term.replace(termReplaceRegex, (_, typeKeyword, whitespace, typeValue) => {
38+
const trimmedTypeKeyword = `${typeKeyword}${whitespace.trim()}`;
39+
40+
// If the type value is already wrapped in quotes, leave it as is
41+
return /['"]/.test(typeValue)
42+
? `${trimmedTypeKeyword}${typeValue}`
43+
: `${trimmedTypeKeyword}"${typeValue}"`;
44+
});
45+
};
46+
47+
const dedupeTypes = (types: FilterValues<string>): FilterValues<string> => [
48+
...new Set(types.map((item) => item.replace(/[-\s]+/g, ' ').trim())),
49+
];
50+
51+
export const parseSearchParams = (term: string, searchableTypes: string[]): ParsedSearchParams => {
2052
const recognizedFields = knownFilters.concat(...Object.values(aliasMap));
2153
let query: Query;
2254

55+
// Finds all multiword types that are separated by whitespace or hyphens
56+
const multiWordSearchableTypesWhitespaceSeperated = searchableTypes
57+
.filter((item) => /[ -]/.test(item))
58+
.map((item) => item.replace(/-/g, ' '));
59+
60+
const modifiedTerm = convertMultiwordTypesToPhrasesAndTrimWhitespace(
61+
term,
62+
multiWordSearchableTypesWhitespaceSeperated
63+
);
64+
2365
try {
24-
query = Query.parse(term, {
66+
query = Query.parse(modifiedTerm, {
2567
schema: { recognizedFields },
2668
});
2769
} catch (e) {
@@ -42,7 +84,7 @@ export const parseSearchParams = (term: string): ParsedSearchParams => {
4284
term: searchTerm,
4385
filters: {
4486
tags: tags ? valuesToString(tags) : undefined,
45-
types: types ? valuesToString(types) : undefined,
87+
types: types ? dedupeTypes(valuesToString(types)) : undefined,
4688
},
4789
};
4890
};

0 commit comments

Comments
 (0)