diff --git a/docs/features/search-results.md b/docs/features/search-results.md index 0391ecf98..0a3edb395 100644 --- a/docs/features/search-results.md +++ b/docs/features/search-results.md @@ -71,24 +71,25 @@ OR ia:whatEvent:"[term]*" OR ia:descriptionEvent:"[term]*" OR lnk:title:"[term]* ### Key facts -If you have entered more than one word into the search input box, then the search query is constructed automatically using an `AND` operation. +1. If you have entered more than one word into the search input box, then the search query is constructed automatically using an `AND` operation. -I you have entered more than one word encapsulated in quotation marks, then the search query is constructed treated everything as a single string. +2. If you have entered more than one word encapsulated in quotation marks, then the search query is constructed treated everything as a single string. -If you have entered more than one word separated by `AND`, then the search query is constructed using an `AND` conjunction. +3. If you have entered more than one word separated by `AND`, then the search query is constructed using an `AND` conjunction. Since `AND` is the default operator (see fact 1), the explicit `AND` keywords are removed when the search input value is processed. -If you have entered more than one word separated by `OR`, then the search query is constructed using an `OR` disjunction. +4. If you have entered more than one word separated by `OR`, then the search query is constructed using an `OR` disjunction. Unlike `AND`, the `OR` operators are preserved when processing the search input value because `OR` is not the default operator. -If you have entered an `=` symbol before the search term, then the search query is constructed using exact term matching. +5. If you have entered an `=` symbol before the search term, then the search query is constructed using exact term matching. **Note:** Works only with Solr search. For Elastic Search consider using Search Logical Filter. ### Examples -| Search Type | Search input | Expected result | -| --- | --- | --- | -| Single Term | banana | Nodes that contain the term **banana** in any content | -| Conjunction | big yellow banana | Nodes that contain all of the terms **big**, **yellow**, and **banana** | -| Phrase | "big yellow banana" | Nodes that contain all of the terms **big**, **yellow**, and **banana** | -| Conjunction | big AND yellow AND banana | Nodes that contain all of the terms **big**, **yellow**, and **banana** | -| Disjunction | orange OR banana OR apple | Nodes that contain at least one of the terms **orange**, **banana** or **apple** | -| Exact term | =orange | Nodes that contain the exact term **orange** in any content | +| Search Type | Entered search input value | Expected result | Processed search input value | +| ----------- | -------------------------- | -------------------------------------------------------------------------------- | ---------------------------- | +| Single Term | banana | Nodes that contain the term **banana** in any content | banana | +| Conjunction | big yellow banana | Nodes that contain all of the terms **big**, **yellow**, and **banana** | big yellow banana | +| Phrase | "big yellow banana" | Nodes that contain the exact phrase **big yellow banana** | "big yellow banana" | +| Conjunction | big AND yellow AND banana | Nodes that contain all of the terms **big**, **yellow**, and **banana** | big yellow banana | +| Disjunction | orange OR banana OR apple | Nodes that contain at least one of the terms **orange**, **banana** or **apple** | orange OR banana OR apple | +| Exact term | =orange | Nodes that contain the exact term **orange** in any content. | orange | +**Important note:** Consider using Search Logical Filter when you need to combine multiple search types. Mixing search types directly in the input may result in wrong query format and incorrect results. diff --git a/projects/aca-content/src/lib/utils/aca-search-utils.spec.ts b/projects/aca-content/src/lib/utils/aca-search-utils.spec.ts index 4503d54c6..b46cc8494 100644 --- a/projects/aca-content/src/lib/utils/aca-search-utils.spec.ts +++ b/projects/aca-content/src/lib/utils/aca-search-utils.spec.ts @@ -54,6 +54,11 @@ describe('SearchUtils', () => { expect(isOperator(null)).toBeFalse(); expect(isOperator(undefined)).toBeFalse(); }); + + it('should treat lowercase operators as search terms', () => { + expect(isOperator('and')).toBeFalse(); + expect(isOperator('or')).toBeFalse(); + }); }); describe('formatSearchTermByFields', () => { @@ -109,8 +114,8 @@ describe('SearchUtils', () => { }); it('should support exact term matching with operators', () => { - expect(formatSearchTerm('=test1.pdf or =test2.pdf', ['cm:name', 'cm:title'])).toBe( - `(=cm:name:"test1.pdf" OR =cm:title:"test1.pdf") or (=cm:name:"test2.pdf" OR =cm:title:"test2.pdf")` + expect(formatSearchTerm('=test1.pdf OR =test2.pdf', ['cm:name', 'cm:title'])).toBe( + `(=cm:name:"test1.pdf" OR =cm:title:"test1.pdf") OR (=cm:name:"test2.pdf" OR =cm:title:"test2.pdf")` ); }); }); @@ -145,9 +150,9 @@ describe('SearchUtils', () => { expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('test'); }); - it('should properly extract search term for custom search', () => { + it('should preserve quotes in search term for custom search', () => { const query = { userQuery: '"test"' }; - expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('test'); + expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('"test"'); }); it('should properly extract search term when userQuery does not contain quotes', () => { @@ -164,6 +169,25 @@ describe('SearchUtils', () => { const query = { userQuery: 'cm:name:"quoted term" AND TEXT:unquoted' }; expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('quoted term TEXT:unquoted'); }); + + it('should handle complex search query', () => { + const query = { + userQuery: `((cm:name:"a*" OR cm:title:"a*" OR cm:description:"a*" OR TEXT:"a*" OR TAG:"a*") AND + (cm:name:"b*" OR cm:title:"b*" OR cm:description:"b*" OR TEXT:"b*" OR TAG:"b*") OR + (cm:name:"c*" OR cm:title:"c*" OR cm:description:"c*" OR TEXT:"c*" OR TAG:"c*"))` + }; + expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('a b OR c'); + }); + + it('should not treat operator as a searched word', () => { + const query = { userQuery: 'AND' }; + expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe(''); + }); + + it('should not unquote when searching for phrase', () => { + const query = { userQuery: '"exact phrase search"' }; + expect(extractSearchedWordFromEncodedQuery(encodeQuery(query))).toBe('"exact phrase search"'); + }); }); describe('extractFiltersFromEncodedQuery', () => { diff --git a/projects/aca-content/src/lib/utils/aca-search-utils.ts b/projects/aca-content/src/lib/utils/aca-search-utils.ts index c3e2f15fc..30d6dfbca 100644 --- a/projects/aca-content/src/lib/utils/aca-search-utils.ts +++ b/projects/aca-content/src/lib/utils/aca-search-utils.ts @@ -30,10 +30,8 @@ */ export function isOperator(input: string): boolean { if (input) { - input = input.trim().toUpperCase(); - const operators = ['AND', 'OR']; - return operators.includes(input); + return operators.includes(input.trim()); } return false; } @@ -111,17 +109,54 @@ export function extractUserQueryFromEncodedQuery(encodedQuery: string): string { * @returns string */ export function extractSearchedWordFromEncodedQuery(encodedQuery: string): string { - if (encodedQuery) { - const userQuery = extractUserQueryFromEncodedQuery(encodedQuery); - return userQuery !== '' && userQuery !== undefined - ? userQuery - .split('AND') - .map((searchCondition) => { - const searchTerm = searchCondition.includes('"') ? searchCondition.split('"')[1] : searchCondition.trim(); - return searchTerm?.endsWith('*') && searchTerm !== '*' ? searchTerm.slice(0, -1) : searchTerm; - }) - .join(' ') - : ''; + if (!encodedQuery) { + return ''; + } + + const userQuery = extractUserQueryFromEncodedQuery(encodedQuery); + if (!userQuery) { + return ''; + } + + const tokenRegex = /\(([^()]+)\)|\b(AND|OR)\b/g; + const fragments: string[] = []; + let match: RegExpExecArray | null; + + while ((match = tokenRegex.exec(userQuery))) { + if (match[1]) { + fragments.push(extractWordFromQuery(match[1])); + } else if (match[2] === 'OR') { + fragments.push('OR'); + } + } + + if (fragments.length === 0) { + return userQuery + .split(/\bAND\b|\bOR\b/) + .map((part) => extractWordFromQuery(part)) + .filter(Boolean) + .join(' ') + .trim(); + } + + return fragments.join(' ').trim(); +} + +/** + * Extracts the searched word from a part of search query + * + * @param queryPart encoded query + * @returns searched word + */ +function extractWordFromQuery(queryPart: string): string { + const regex = /:"([^"]+)"/; + const quoted = regex.exec(queryPart); + if (quoted) { + return quoted[1].replace(/\*$/, ''); + } + const trimmedPart = queryPart.trim(); + if (trimmedPart && !isOperator(trimmedPart)) { + return trimmedPart; } return ''; }