Skip to content

Commit

Permalink
[8.x] [Global Search] Add multiword type handling in global search (#…
Browse files Browse the repository at this point in the history
…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":"[email protected]"},"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 <[email protected]>
  • Loading branch information
kibanamachine and kowalczyk-krzysztof authored Oct 16, 2024
1 parent 74b1ca6 commit b75aac6
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const SearchBar: FC<SearchBarProps> = (opts) => {
reportEvent.searchRequest();
}

const rawParams = parseSearchParams(searchValue.toLowerCase());
const rawParams = parseSearchParams(searchValue.toLowerCase(), searchableTypes);
let tagIds: string[] | undefined;
if (taggingApi && rawParams.filters.tags) {
tagIds = rawParams.filters.tags.map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@ import { parseSearchParams } from './parse_search_params';

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

it('returns the raw query as `term` in case of parsing error', () => {
const searchParams = parseSearchParams('tag:((()^invalid');
const searchParams = parseSearchParams('tag:((()^invalid', []);
expect(searchParams).toEqual({
term: 'tag:((()^invalid',
filters: {},
});
});

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

it('returns correct filters when no field clause is defined', () => {
const searchParams = parseSearchParams('hello');
const searchParams = parseSearchParams('hello', []);
expect(searchParams.filters).toEqual({
tags: undefined,
types: undefined,
});
});

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

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

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

it('converts boolean and number values to string for known filters', () => {
const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello');
const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello', []);
expect(searchParams).toEqual({
term: 'hello',
filters: {
Expand All @@ -76,4 +76,74 @@ describe('parseSearchParams', () => {
},
});
});

it('converts multiword searchable types to phrases so they get picked up as types', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad', 'enterprise search'];
const searchParams = parseSearchParams(
'type:canvas workpad types:canvas-workpad hello type:enterprise search type:not multiword',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello multiword',
filters: {
types: ['canvas workpad', 'enterprise search', 'not'],
},
});
});

it('parses correctly when multiword types are already quoted', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad'];
const searchParams = parseSearchParams(
`type:"canvas workpad" hello type:"dashboard"`,
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['canvas workpad', 'dashboard'],
},
});
});

it('parses correctly when there is whitespace between type keyword and value', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad'];
const searchParams = parseSearchParams(
'type: canvas workpad hello type: dashboard',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['canvas workpad', 'dashboard'],
},
});
});

it('dedupes duplicate types', () => {
const mockSearchableMultiwordTypes = ['canvas-workpad'];
const searchParams = parseSearchParams(
'type:canvas workpad hello type:dashboard type:canvas-workpad type:canvas workpad type:dashboard',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['canvas workpad', 'dashboard'],
},
});
});

it('handles whitespace removal even if there are no multiword types', () => {
const mockSearchableMultiwordTypes: string[] = [];
const searchParams = parseSearchParams(
'hello type: dashboard',
mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello',
filters: {
types: ['dashboard'],
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,54 @@ const aliasMap = {
type: ['types'],
};

export const parseSearchParams = (term: string): ParsedSearchParams => {
// 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.
const convertMultiwordTypesToPhrasesAndTrimWhitespace = (
term: string,
multiWordTypes: string[]
): string => {
if (!multiWordTypes.length) {
return term.replace(
/(type:|types:)\s*([^"']*?)\b([^"'\s]+)/gi,
(_, typeKeyword, whitespace, typeValue) => `${typeKeyword}${whitespace.trim()}${typeValue}`
);
}

const typesPattern = multiWordTypes.join('|');
const termReplaceRegex = new RegExp(
`(type:|types:)\\s*([^"']*?)\\b((${typesPattern})\\b|[^\\s"']+)`,
'gi'
);

return term.replace(termReplaceRegex, (_, typeKeyword, whitespace, typeValue) => {
const trimmedTypeKeyword = `${typeKeyword}${whitespace.trim()}`;

// If the type value is already wrapped in quotes, leave it as is
return /['"]/.test(typeValue)
? `${trimmedTypeKeyword}${typeValue}`
: `${trimmedTypeKeyword}"${typeValue}"`;
});
};

const dedupeTypes = (types: FilterValues<string>): FilterValues<string> => [
...new Set(types.map((item) => item.replace(/[-\s]+/g, ' ').trim())),
];

export const parseSearchParams = (term: string, searchableTypes: string[]): ParsedSearchParams => {
const recognizedFields = knownFilters.concat(...Object.values(aliasMap));
let query: Query;

// Finds all multiword types that are separated by whitespace or hyphens
const multiWordSearchableTypesWhitespaceSeperated = searchableTypes
.filter((item) => /[ -]/.test(item))
.map((item) => item.replace(/-/g, ' '));

const modifiedTerm = convertMultiwordTypesToPhrasesAndTrimWhitespace(
term,
multiWordSearchableTypesWhitespaceSeperated
);

try {
query = Query.parse(term, {
query = Query.parse(modifiedTerm, {
schema: { recognizedFields },
});
} catch (e) {
Expand All @@ -42,7 +84,7 @@ export const parseSearchParams = (term: string): ParsedSearchParams => {
term: searchTerm,
filters: {
tags: tags ? valuesToString(tags) : undefined,
types: types ? valuesToString(types) : undefined,
types: types ? dedupeTypes(valuesToString(types)) : undefined,
},
};
};
Expand Down

0 comments on commit b75aac6

Please sign in to comment.