Skip to content

Commit

Permalink
STSMACOM-831 <AdvancedSearch> Improve algorithm of splitting query …
Browse files Browse the repository at this point in the history
…string into rows (#1472)

* STSMACOM-831 `<AdvancedSearch>` Improve algorithm of splitting query string into rows

* STSMACOM-831 When user enters advanced search with no option or match Advanced search row should have default search option and match

(cherry picked from commit bc18739)
  • Loading branch information
BogdanDenis authored and zburke committed May 6, 2024
1 parent 6a055c5 commit 95dd4af
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 24 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
* Safely render user-provided markup in `<NotesView>` component. Fixes STSMACOM-816.
* Do not trigger logic for auto-opening the record's view screen in `<SearchAndSort>` if URL already contains the record ID. Fixes STSMACOM-822.

## [9.1.1] (IN PROGRESS)

* `<AdvancedSearch>` Improve algorithm of splitting query string into rows. Refs STSMACOM-831.

## [9.1.0](https://github.com/folio-org/stripes-smart-components/tree/v9.1.0) (2024-03-13)
[Full Changelog](https://github.com/folio-org/stripes-smart-components/compare/v9.0.1...v9.1.0)

Expand Down
72 changes: 48 additions & 24 deletions lib/SearchAndSort/advancedSearchQueryToRows.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,67 @@
import {
MATCH_OPTIONS,
BOOLEAN_OPERATORS,
DEFAULT_SEARCH_OPTION,
MATCH_OPTIONS,
} from '@folio/stripes-components/lib/AdvancedSearch';

const advancedSearchQueryToRows = (queryValue) => {
if (!queryValue) {
return [];
}

const splitIntoRowsRegex = /(?=\sor\s|\sand\s|\snot\s)/g;
/*
Algorithm for parsing string into rows is as follows:
1. We split the string into an arry of tokens, using space as a delimiter
2. With a window of size 3 we iterate over the array and see if the window is over a specific part of a query string that represents a beginning of a new row
Size 3 is chosen because a new row starts with 3 special tokens: a boolean condition, a search option and a match parameter.
3. If a first token is a boolean token (and/or/not) and a third token is a match parameter (exactPhrase/startsWith/etc...)
we consider it a beginning of a new row and add it to array of rows.
Ideally we also need to check that a second token is a search option, but it needs to be passed by each application specifically because different apps
have different search options. That would make it a breaking change so for now we only use boolean and match option
4. If we added a new row - move the window by 3 tokens (the actual implementation inside `if` we increment by 2, but that is because we increment by 1 after each iteration which would be 3 in total).
We need to move by 3 because if we only moved by 1 or 2 the condition would fail and we would add special tokens to query value
5. If we didn't add a new row - then we add first token to the last added row's query
6. Move the windows by 1 and repeat
*/

const tokens = queryValue.replace(/\s+/g, ' ').split(' ');

// split will return array of strings:
// ['keyword==test', 'or issn=123', ...]
const matches = queryValue.split(splitIntoRowsRegex).map(i => i.trim());
const rows = [];
for (let index = 0; index <= tokens.length;) {
const token1 = tokens[index - 1];
const token2 = tokens[index];
const token3 = tokens[index + 1];

return matches.map((match, index) => {
let bool = '';
let query = match;
const isFirstTokenBoolean = Object.values(BOOLEAN_OPERATORS).includes(token1);
const isThirdTokenMatch = Object.values(MATCH_OPTIONS).includes(token3);

// first row doesn't have a bool operator
if (index !== 0) {
bool = match.substr(0, match.indexOf(' '));
query = match.substr(bool.length).trim();
if ((isFirstTokenBoolean || !token1) && isThirdTokenMatch) {
rows.push({
bool: token1 || '',
searchOption: token2,
match: token3,
query: '',
});

index += 2;
} else if (rows[rows.length - 1]) {
rows[rows.length - 1].query += `${token1} `;
}
index += 1;
}

const matchOperatorStartIndex = query.indexOf(' ') + 1;
const matchOperatorEndIndex = query.substr(matchOperatorStartIndex).indexOf(' ') + matchOperatorStartIndex + 1;
if (!rows.length) {
rows.push({
query: queryValue,
bool: '',
searchOption: DEFAULT_SEARCH_OPTION,
match: MATCH_OPTIONS.CONTAINS_ALL,
});
}

const option = query.substr(0, matchOperatorStartIndex).trim().replaceAll('"', '');
const _match = query.substring(matchOperatorStartIndex, matchOperatorEndIndex).trim().replaceAll('"', '');
const value = query.substring(matchOperatorEndIndex).trim().replaceAll('"', '');
rows.forEach(row => { row.query = row.query.trim(); });

return {
query: value,
bool,
searchOption: option || DEFAULT_SEARCH_OPTION,
match: _match || MATCH_OPTIONS.CONTAINS_ALL,
};
});
return rows;
};

export default advancedSearchQueryToRows;
39 changes: 39 additions & 0 deletions lib/SearchAndSort/tests/advancedSearchQueryToRows-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,45 @@ describe('advancedSearchQueryToRows', () => {
});
});

describe('when query contains boolean option as part of value', () => {
it('should parse query correctly', () => {
const query = 'keyword containsAny some text and some other text';

expect(advancedSearchQueryToRows(query)).to.deep.include({
query: 'some text and some other text',
bool: '',
searchOption: 'keyword',
match: 'containsAny',
});
});
});

describe('when query contains boolean option and match option as part of value, but no search option', () => {
it('should parse query correctly', () => {
const query = 'keyword containsAny some text and containsAny';

expect(advancedSearchQueryToRows(query)).to.deep.include({
query: 'some text and containsAny',
bool: '',
searchOption: 'keyword',
match: 'containsAny',
});
});
});

describe('when query contains search option and match option as part of value, but no boolean', () => {
it('should parse query correctly', () => {
const query = 'keyword containsAny some text keyword containsAny some other text';

expect(advancedSearchQueryToRows(query)).to.deep.include({
query: 'some text keyword containsAny some other text',
bool: '',
searchOption: 'keyword',
match: 'containsAny',
});
});
});

describe('when query without match and search option', () => {
it('should use default value for match and search option', () => {
const query = 'test';
Expand Down

0 comments on commit 95dd4af

Please sign in to comment.