diff --git a/docs/features/search-results.md b/docs/features/search-results.md index 45b381751..848dbd003 100644 --- a/docs/features/search-results.md +++ b/docs/features/search-results.md @@ -18,3 +18,34 @@ This page consists of the following ADF components: - [Toolbar with basic actions](/features/document-list-layout#actions-and-the-actions-toolbar) like `Preview`, `Download`, `Favorite`, `Copy`, etc. And also the Info Drawer, Toolbar and Node Selector dialogs for copy and move operations. + +## Alfresco Full Text Search + +The following table describes current support of the +[Alfresco Full Text Search](http://docs.alfresco.com/6.0/concepts/rm-searchsyntax-intro.html) (FTS) syntax +in the Content Application when using **Search Input** component. + +| Feature | Full | Partial | N/A | Details | +| ---------------------------------------------------------------- | ---- | ------- | --- | ---------------------------------------------------------------------------------- | +| Search for a single term | X | | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-single.html) | +| Search for a phrase | | X | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-phrase.html) | +| Search for an exact term | X | | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-exact.html) | +| Search for term expansion | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-term.html) | +| Search for conjunctions | X | | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-conjunct.html) | +| Search for disjunctions | X | | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-disjunct.html) | +| Search for negation | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-negate.html) | +| Search for optional, mandatory, and excluded elements of a query | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-optional.html) | +| Search in fields | | X | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-fields.html) | +| Search for wildcards | | X | | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-wildcards.html) | +| Search for ranges | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-ranges.html) | +| Search for fuzzy matching | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-fuzzy.html) | +| Search for proximity | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-proximity.html) | +| Search for boosts | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-boosts.html) | +| Search for grouping | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-grouping.html) | +| Search for spans and positions | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-spans.html) | +| Escaping characters | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-escaping.html) | +| Mixed FTS ID behavior | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-ftsid.html) | +| Search for operator precedence | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-precedence.html) | +| Search query templates | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-querytemplates.html) | +| Search query literals | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-literals.html) | +| Search using date math | | | X | [Docs](https://docs.alfresco.com/6.0/concepts/rm-searchsyntax-datemaths.html) | diff --git a/src/app.config.json b/src/app.config.json index c3de27da7..ae2034151 100644 --- a/src/app.config.json +++ b/src/app.config.json @@ -119,10 +119,6 @@ "cm:name", "cm:title", "cm:description", - "ia:whatEvent", - "ia:descriptionEvent", - "lnk:title", - "lnk:description", "TEXT", "TAG" ], diff --git a/src/app/components/search/search-results/search-results.component.spec.ts b/src/app/components/search/search-results/search-results.component.spec.ts index 3c22a2478..754005081 100644 --- a/src/app/components/search/search-results/search-results.component.spec.ts +++ b/src/app/components/search/search-results/search-results.component.spec.ts @@ -35,11 +35,16 @@ describe('SearchComponent', () => { expect(component.formatSearchQuery('')).toBeNull(); }); - it('should use original user input if content contains colons', () => { + it('should use original user input if text contains colons', () => { const query = 'TEXT:test OR TYPE:folder'; expect(component.formatSearchQuery(query)).toBe(query); }); + it('should use original user input if text contains quotes', () => { + const query = `"Hello World"`; + expect(component.formatSearchQuery(query)).toBe(query); + }); + it('should format user input according to the configuration fields', () => { config.config = { search: { @@ -48,7 +53,7 @@ describe('SearchComponent', () => { }; const query = component.formatSearchQuery('hello'); - expect(query).toBe(`cm:name:"hello*" OR cm:title:"hello*"`); + expect(query).toBe(`(cm:name:"hello*" OR cm:title:"hello*")`); }); it('should format user input as cm:name if configuration not provided', () => { @@ -59,7 +64,75 @@ describe('SearchComponent', () => { }; const query = component.formatSearchQuery('hello'); - expect(query).toBe(`cm:name:"hello*"`); + expect(query).toBe(`(cm:name:"hello*")`); + }); + + it('should use AND operator when conjunction has no operators', () => { + config.config = { + search: { + 'aca:fields': ['cm:name'] + } + }; + + const query = component.formatSearchQuery('big yellow banana'); + + expect(query).toBe( + `(cm:name:"big*") AND (cm:name:"yellow*") AND (cm:name:"banana*")` + ); + }); + + it('should support conjunctions with AND operator', () => { + config.config = { + search: { + 'aca:fields': ['cm:name', 'cm:title'] + } + }; + + const query = component.formatSearchQuery('big AND yellow AND banana'); + + expect(query).toBe( + `(cm:name:"big*" OR cm:title:"big*") AND (cm:name:"yellow*" OR cm:title:"yellow*") AND (cm:name:"banana*" OR cm:title:"banana*")` + ); + }); + + it('should support conjunctions with OR operator', () => { + config.config = { + search: { + 'aca:fields': ['cm:name', 'cm:title'] + } + }; + + const query = component.formatSearchQuery('big OR yellow OR banana'); + + expect(query).toBe( + `(cm:name:"big*" OR cm:title:"big*") OR (cm:name:"yellow*" OR cm:title:"yellow*") OR (cm:name:"banana*" OR cm:title:"banana*")` + ); + }); + + it('should support exact term matching with default fields', () => { + config.config = { + search: { + 'aca:fields': ['cm:name', 'cm:title'] + } + }; + + const query = component.formatSearchQuery('=orange'); + + expect(query).toBe(`(=cm:name:"orange" OR =cm:title:"orange")`); + }); + + it('should support exact term matching with operators', () => { + config.config = { + search: { + 'aca:fields': ['cm:name', 'cm:title'] + } + }; + + const query = component.formatSearchQuery('=test1.pdf or =test2.pdf'); + + expect(query).toBe( + `(=cm:name:"test1.pdf" OR =cm:title:"test1.pdf") or (=cm:name:"test2.pdf" OR =cm:title:"test2.pdf")` + ); }); it('should navigate to folder on double click', () => { diff --git a/src/app/components/search/search-results/search-results.component.ts b/src/app/components/search/search-results/search-results.component.ts index 8892cc7c3..6b4d552a7 100644 --- a/src/app/components/search/search-results/search-results.component.ts +++ b/src/app/components/search/search-results/search-results.component.ts @@ -114,6 +114,33 @@ export class SearchResultsComponent extends PageComponent implements OnInit { } } + private isOperator(input: string): boolean { + if (input) { + input = input.trim().toUpperCase(); + + const operators = ['AND', 'OR']; + return operators.includes(input); + } + return false; + } + + private formatFields(fields: string[], term: string): string { + let prefix = ''; + let suffix = '*'; + + if (term.startsWith('=')) { + prefix = '='; + suffix = ''; + term = term.substring(1); + } + + return ( + '(' + + fields.map(field => `${prefix}${field}:"${term}${suffix}"`).join(' OR ') + + ')' + ); + } + formatSearchQuery(userInput: string) { if (!userInput) { return null; @@ -121,14 +148,28 @@ export class SearchResultsComponent extends PageComponent implements OnInit { userInput = userInput.trim(); - if (userInput.includes(':')) { + if (userInput.includes(':') || userInput.includes('"')) { return userInput; } const fields = this.config.get('search.aca:fields', ['cm:name']); - const query = fields.map(field => `${field}:"${userInput}*"`).join(' OR '); + const words = userInput.split(' '); - return query; + if (words.length > 1) { + const separator = words.some(this.isOperator) ? ' ' : ' AND '; + + return words + .map(term => { + if (this.isOperator(term)) { + return term; + } + + return this.formatFields(fields, term); + }) + .join(separator); + } + + return this.formatFields(fields, userInput); } onSearchResultLoaded(nodePaging: NodePaging) {