Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Global Search] Add multiword type handling in global search #196087

Merged
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,18 @@ 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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add type:"canvas workpad" to the list so we make sure it handles types with existing quotes?

Also by doing so, I realized that we pass twice the same types in the array, could we avoid dedupes when returning the array? (I know it's not part of fixing the issue but we could use the momentum 😊). Thanks 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I didn't think about checking for already existing quotes. It should work but I'll cover it in the test.

As for deduping, I wasn't really sure if this is what we want (although it's makes logical sense) but sure, I'll add that.

mockSearchableMultiwordTypes
);
expect(searchParams).toEqual({
term: 'hello multiword',
filters: {
types: ['canvas workpad', 'enterprise search', 'not', 'canvas-workpad'],
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,40 @@ const aliasMap = {
type: ['types'],
};

export const parseSearchParams = (term: string): ParsedSearchParams => {
// Converts multi word types to phrases by wrapping them in quotes. Example: type:canvas workpad -> type:"canvas workpad"
const convertMultiwordTypesToPhrases = (term: string, multiWordTypes: string[]): string => {
if (multiWordTypes.length === 0) {
return term;
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we use a generic name, e.g. termReplaceRegex?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, thanks for noticing this. This variable name is something I used initially, before I made this solution more generic. I'll change that.


const modifiedTerm = term.replace(
canvasWorkpadRegex,
(match, typePrefix, additionalTextInBetween, matchedPhrase) =>
`${typePrefix}${additionalTextInBetween}"${matchedPhrase}"`
);

return modifiedTerm;
};

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 = convertMultiwordTypesToPhrases(
term,
multiWordSearchableTypesWhitespaceSeperated
);

try {
query = Query.parse(term, {
query = Query.parse(modifiedTerm, {
schema: { recognizedFields },
});
} catch (e) {
Expand Down