Skip to content

Commit 4081f33

Browse files
authored
Merge pull request #28 from netgrif/NAE-1614
[NAE-1614] Advanced search substring query
2 parents 8f79859 + b42843d commit 4081f33

File tree

10 files changed

+89
-20
lines changed

10 files changed

+89
-20
lines changed

projects/netgrif-components-core/src/lib/search/models/category/case/case-dataset.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,13 @@ export class CaseDataset extends Category<Datafield> implements AutocompleteOpti
260260
case 'file':
261261
case 'fileList':
262262
return resolver.getIndex(datafield.fieldId, SearchIndex.FILE_NAME,
263-
this.isSelectedOperator(Equals) || this.isSelectedOperator(NotEquals));
263+
this.isSelectedOperator(Equals) || this.isSelectedOperator(NotEquals) || this.isSelectedOperator(Substring));
264264
case 'user':
265265
case 'userList':
266266
return resolver.getIndex(datafield.fieldId, SearchIndex.USER_ID);
267267
default:
268268
return resolver.getIndex(datafield.fieldId, SearchIndex.FULLTEXT,
269-
this.isSelectedOperator(Equals) || this.isSelectedOperator(NotEquals));
269+
this.isSelectedOperator(Equals) || this.isSelectedOperator(NotEquals) || this.isSelectedOperator(Substring));
270270
}
271271
}
272272

@@ -417,10 +417,6 @@ export class CaseDataset extends Category<Datafield> implements AutocompleteOpti
417417
return value;
418418
}
419419

420-
protected isSelectedOperator(operatorClass: Type<any>): boolean {
421-
return this.selectedOperator === this._operatorService.getOperator(operatorClass);
422-
}
423-
424420
serializeClass(): Categories | string {
425421
return Categories.CASE_DATASET;
426422
}

projects/netgrif-components-core/src/lib/search/models/category/case/case-simple-dataset.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ export class CaseSimpleDataset extends NoConfigurationCategory<string> {
8686
break;
8787
case 'file':
8888
case 'fileList':
89-
this._elasticKeyword = resolver.getIndex(this._fieldId, SearchIndex.FILE_NAME);
89+
this._elasticKeyword = resolver.getIndex(this._fieldId, SearchIndex.FILE_NAME, this.isSelectedOperator(Substring));
9090
break;
9191
case 'user':
9292
case 'userList':
9393
this._elasticKeyword = resolver.getIndex(this._fieldId, SearchIndex.USER_ID);
9494
break;
9595
default:
96-
this._elasticKeyword = resolver.getIndex(this._fieldId, SearchIndex.FULLTEXT);
96+
this._elasticKeyword = resolver.getIndex(this._fieldId, SearchIndex.FULLTEXT, this.isSelectedOperator(Substring));
9797
}
9898
}
9999

projects/netgrif-components-core/src/lib/search/models/category/case/case-title.spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@ import {configureCategory} from '../../../../utility/tests/utility/configure-cat
55
import {Equals} from '../../operator/equals';
66
import {Categories} from '../categories';
77
import {Operators} from '../../operator/operators';
8+
import {Substring} from '../../operator/substring';
9+
import {CaseSearch} from './case-search.enum';
10+
import {SearchIndexResolverService} from '../../../search-keyword-resolver-service/search-index-resolver.service';
11+
import {OptionalDependencies} from '../../../category-factory/optional-dependencies';
812

913
describe('CaseTitle', () => {
1014
let category: CaseTitle;
1115
let operatorService: OperatorService;
16+
let deps: OptionalDependencies;
17+
1218

1319
beforeEach(() => {
1420
operatorService = new OperatorService(new OperatorResolverService());
15-
category = new CaseTitle(operatorService, null);
21+
deps = {searchIndexResolver: new SearchIndexResolverService()} as OptionalDependencies;
22+
category = new CaseTitle(operatorService, null, deps);
1623
});
1724

1825
afterEach(() => {
@@ -64,4 +71,10 @@ describe('CaseTitle', () => {
6471
done();
6572
});
6673
});
74+
75+
it('should use keyword index', () => {
76+
configureCategory(category, operatorService, Substring, ['foo']);
77+
const predicate = category.generatePredicate(['input']);
78+
expect(predicate.query.value.includes(`${CaseSearch.TITLE}${deps.searchIndexResolver.KEYWORD}`)).toBeTrue();
79+
});
6780
});

projects/netgrif-components-core/src/lib/search/models/category/case/case-title.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import {NotEquals} from '../../operator/not-equals';
88
import {Like} from '../../operator/like';
99
import {Categories} from '../categories';
1010
import {CaseSearch} from './case-search.enum';
11+
import {OptionalDependencies} from '../../../category-factory/optional-dependencies';
1112

1213
export class CaseTitle extends NoConfigurationCategory<string> {
1314

1415
private static readonly _i18n = 'search.category.case.title';
1516

16-
constructor(operators: OperatorService, logger: LoggerService) {
17+
constructor(operators: OperatorService, logger: LoggerService, protected _optionalDependencies?: OptionalDependencies) {
1718
super([CaseSearch.TITLE],
1819
[
1920
operators.getOperator(Substring),
@@ -32,10 +33,19 @@ export class CaseTitle extends NoConfigurationCategory<string> {
3233
}
3334

3435
duplicate(): CaseTitle {
35-
return new CaseTitle(this._operatorService, this._log);
36+
return new CaseTitle(this._operatorService, this._log, this._optionalDependencies);
3637
}
3738

3839
serializeClass(): Categories | string {
3940
return Categories.CASE_TITLE;
4041
}
42+
43+
protected get elasticKeywords(): Array<string> {
44+
if (!!this._optionalDependencies) {
45+
const resolver = this._optionalDependencies.searchIndexResolver;
46+
return [resolver.getCoreIndex(CaseSearch.TITLE, this.isSelectedOperator(Substring))];
47+
} else {
48+
return this._elasticKeywords;
49+
}
50+
}
4151
}

projects/netgrif-components-core/src/lib/search/models/category/case/case-visual-id.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ import {configureCategory} from '../../../../utility/tests/utility/configure-cat
55
import {Equals} from '../../operator/equals';
66
import {Categories} from '../categories';
77
import {Operators} from '../../operator/operators';
8+
import {Substring} from '../../operator/substring';
9+
import {SearchIndexResolverService} from '../../../search-keyword-resolver-service/search-index-resolver.service';
10+
import {CaseSearch} from './case-search.enum';
11+
import {OptionalDependencies} from '../../../category-factory/optional-dependencies';
812

913
describe('CaseVisualId', () => {
1014
let category: CaseVisualId;
1115
let operatorService: OperatorService;
16+
let deps: OptionalDependencies;
1217

1318
beforeEach(() => {
1419
operatorService = new OperatorService(new OperatorResolverService());
15-
category = new CaseVisualId(operatorService, null);
20+
deps = {searchIndexResolver: new SearchIndexResolverService()} as OptionalDependencies;
21+
category = new CaseVisualId(operatorService, null, deps);
1622
});
1723

1824
afterEach(() => {
@@ -64,4 +70,10 @@ describe('CaseVisualId', () => {
6470
done();
6571
});
6672
});
73+
74+
it('should use keyword index', () => {
75+
configureCategory(category, operatorService, Substring, ['foo']);
76+
const predicate = category.generatePredicate(['input']);
77+
expect(predicate.query.value.includes(`${CaseSearch.VISUAL_ID}${deps.searchIndexResolver.KEYWORD}`)).toBeTrue();
78+
});
6779
});

projects/netgrif-components-core/src/lib/search/models/category/case/case-visual-id.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import {Equals} from '../../operator/equals';
77
import {NotEquals} from '../../operator/not-equals';
88
import {Categories} from '../categories';
99
import {CaseSearch} from './case-search.enum';
10+
import {OptionalDependencies} from '../../../category-factory/optional-dependencies';
1011

1112
export class CaseVisualId extends NoConfigurationCategory<string> {
1213

1314
private static readonly _i18n = 'search.category.case.visualId';
1415

15-
constructor(operators: OperatorService, logger: LoggerService) {
16+
constructor(operators: OperatorService, logger: LoggerService, protected _optionalDependencies?: OptionalDependencies) {
1617
super([CaseSearch.VISUAL_ID],
1718
[operators.getOperator(Substring), operators.getOperator(Equals), operators.getOperator(NotEquals)],
1819
`${CaseVisualId._i18n}.name`,
@@ -26,10 +27,19 @@ export class CaseVisualId extends NoConfigurationCategory<string> {
2627
}
2728

2829
duplicate(): CaseVisualId {
29-
return new CaseVisualId(this._operatorService, this._log);
30+
return new CaseVisualId(this._operatorService, this._log, this._optionalDependencies);
3031
}
3132

3233
serializeClass(): Categories | string {
3334
return Categories.CASE_VISUAL_ID;
3435
}
36+
37+
protected get elasticKeywords(): Array<string> {
38+
if (!!this._optionalDependencies) {
39+
const resolver = this._optionalDependencies.searchIndexResolver;
40+
return [resolver.getCoreIndex(CaseSearch.VISUAL_ID, this.isSelectedOperator(Substring))];
41+
} else {
42+
return this._elasticKeywords;
43+
}
44+
}
3545
}

projects/netgrif-components-core/src/lib/search/models/category/category.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {Operators} from '../operator/operators';
1616
import {ofVoid} from '../../../utility/of-void';
1717
import {FilterTextSegment} from '../persistance/filter-text-segment';
1818
import {DATE_FORMAT_STRING, DATE_TIME_FORMAT_STRING} from '../../../moment/time-formats';
19+
import {Type} from '@angular/core';
1920

2021
/**
2122
* The top level of abstraction in search query generation. Represents a set of indexed fields that can be searched.
@@ -323,7 +324,7 @@ export abstract class Category<T> {
323324
if (!this.isOperatorSelected()) {
324325
throw new Error('Category cannot generate a Query if no Operator is selected');
325326
}
326-
return this._operatorFormControl.value.createQuery(this._elasticKeywords, userInput as unknown as Array<string>);
327+
return this._operatorFormControl.value.createQuery(this.elasticKeywords, userInput as unknown as Array<string>);
327328
}
328329

329330
/**
@@ -667,4 +668,13 @@ export abstract class Category<T> {
667668
}
668669
return {segment, bold: true};
669670
}
671+
672+
/**
673+
* Checks for selected operator
674+
* @param operatorClass the operator to be checked
675+
* @return boolean of the statement
676+
*/
677+
protected isSelectedOperator(operatorClass: Type<any>): boolean {
678+
return this.selectedOperator === this._operatorService.getOperator(operatorClass);
679+
}
670680
}

projects/netgrif-components-core/src/lib/search/models/operator/substring.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export class Substring extends Operator<string> {
1616
// TODO IMPROVEMENT 27.4.2020 - we could use regular expressions to search for substrings which would solve the unintuitive
1717
// behavior that occurs when we search for strings that contain spaces. We need to escape the input string in a special way
1818
// if we choose to do this
19-
const escapedValue = Operator.escapeInput(args[0]).value;
20-
return Operator.forEachKeyword(elasticKeywords, keyword => new Query(`(${keyword}:\\*${escapedValue}\\*)`));
19+
const escapedValue = Operator.escapeInput(args[0]).value.replace(' ', '\\ ');
20+
return Operator.forEachKeyword(elasticKeywords, keyword => new Query(`(${keyword}:*${escapedValue}*)`));
2121
}
2222

2323
getOperatorNameTemplate(): Array<string> {

projects/netgrif-components-core/src/lib/search/search-keyword-resolver-service/search-index-resolver.service.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {SearchIndex} from '../models/search-index';
1414
providedIn: 'root'
1515
})
1616
export class SearchIndexResolverService {
17-
constructor() { }
17+
18+
public readonly KEYWORD = '.keyword';
19+
20+
constructor() { }
1821

1922
/**
2023
* Constructs the index name for the specified field.
@@ -28,6 +31,20 @@ export class SearchIndexResolverService {
2831
* @returns the full name of the specified index
2932
*/
3033
public getIndex(dataFieldIdentifier: string, index: SearchIndex, keyword = false): string {
31-
return `dataSet.${dataFieldIdentifier}.${index}${keyword ? '.keyword' : ''}`;
34+
return `dataSet.${dataFieldIdentifier}.${index}${keyword ? this.KEYWORD : ''}`;
35+
}
36+
37+
/**
38+
* Constructs the index name for the specified core field.
39+
*
40+
* Note that almost all combinations are valid and will not throw an error when used to query the database, but not all combinations are
41+
* used by the application engine. The {@link SearchIndex} class has some information about which field types map to which indices, but
42+
* you should consult the backend documentation for more reliable information.
43+
* @param index the queried index
44+
* @param keyword whether the keyword of a TEXT index should be queried
45+
* @returns the full name of the specified index
46+
*/
47+
public getCoreIndex(index: string, keyword = false): string {
48+
return `${index}${keyword ? this.KEYWORD : ''}`;
3249
}
3350
}

projects/netgrif-components-core/src/lib/search/search-service/search.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ export class SearchService implements OnDestroy {
257257
* @param searchedSubstring value that should be searched on all full text fields
258258
*/
259259
public setFullTextFilter(searchedSubstring: string): void {
260-
this._fullTextFilter = new SimpleFilter('', this._baseFilter.type, {fullText: searchedSubstring});
260+
const whiteSpacedSubstring = searchedSubstring.replace(' ', '\\ ');
261+
this._fullTextFilter = new SimpleFilter('', this._baseFilter.type, {fullText: whiteSpacedSubstring});
261262
this.updateActiveFilter();
262263
}
263264

0 commit comments

Comments
 (0)