diff --git a/src/backend/model/database/sql/SearchManager.ts b/src/backend/model/database/sql/SearchManager.ts index dd7b1774..af70b4bb 100644 --- a/src/backend/model/database/sql/SearchManager.ts +++ b/src/backend/model/database/sql/SearchManager.ts @@ -274,9 +274,7 @@ export class SearchManager implements ISearchManager { if (typeof (query).value === 'undefined') { throw new Error('Invalid search query: Date Query should contain from value'); } - const whereFN = (query).negate ? 'orWhere' : 'andWhere'; const relation = (query).negate ? '<' : '>='; - const relationRev = (query).negate ? '>' : '<='; const textParam: any = {}; textParam['from' + paramCounter.value] = (query).value; diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts index 81a5e4a8..14623f55 100644 --- a/src/common/entities/SearchQueryDTO.ts +++ b/src/common/entities/SearchQueryDTO.ts @@ -2,7 +2,7 @@ import {GPSMetadata} from './PhotoDTO'; import {Utils} from '../Utils'; export enum SearchQueryTypes { - AND = 1, OR, SOME_OF, + AND = 1, OR, SOME_OF, UNKNOWN_RELATION = 99999, // non-text metadata // |- range types @@ -120,17 +120,22 @@ export namespace SearchQueryDTO { } }; - export const parse = (str: string): SearchQueryDTO => { - console.log(str); + export const parse = (str: string, implicitOR = true): SearchQueryDTO => { + console.log('parsing: ' + str); str = str.replace(/\s\s+/g, ' ') // remove double spaces .replace(/:\s+/g, ':').replace(/\)(?=\S)/g, ') ').trim(); if (str.charAt(0) === '(' && str.charAt(str.length - 1) === ')') { str = str.slice(1, str.length - 1); } - const fistNonBRSpace = () => { + const fistSpace = (start = 0) => { const bracketIn = []; - for (let i = 0; i < str.length; ++i) { + let quotationMark = false; + for (let i = start; i < str.length; ++i) { + if (str.charAt(i) === '"') { + quotationMark = !quotationMark; + continue; + } if (str.charAt(i) === '(') { bracketIn.push(i); continue; @@ -140,7 +145,9 @@ export namespace SearchQueryDTO { continue; } - if (bracketIn.length === 0 && str.charAt(i) === ' ') { + if (quotationMark === false && + bracketIn.length === 0 && + str.charAt(i) === ' ') { return i; } } @@ -148,34 +155,41 @@ export namespace SearchQueryDTO { }; // tokenize - const tokenEnd = fistNonBRSpace(); + const tokenEnd = fistSpace(); if (tokenEnd !== str.length - 1) { if (str.startsWith(' and', tokenEnd)) { return { type: SearchQueryTypes.AND, - list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets - SearchQueryDTO.parse(str.slice(tokenEnd + 4))] + list: [SearchQueryDTO.parse(str.slice(0, tokenEnd), implicitOR), // trim brackets + SearchQueryDTO.parse(str.slice(tokenEnd + 4), implicitOR)] }; - } else { - let padding = 0; - if (str.startsWith(' or', tokenEnd)) { - padding = 3; - } + } else if (str.startsWith(' or', tokenEnd)) { return { type: SearchQueryTypes.OR, - list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets - SearchQueryDTO.parse(str.slice(tokenEnd + padding))] + list: [SearchQueryDTO.parse(str.slice(0, tokenEnd), implicitOR), // trim brackets + SearchQueryDTO.parse(str.slice(tokenEnd + 3), implicitOR)] + }; + } else { // Relation cannot be detected + return { + type: implicitOR === true ? SearchQueryTypes.OR : SearchQueryTypes.UNKNOWN_RELATION, + list: [SearchQueryDTO.parse(str.slice(0, tokenEnd), implicitOR), // trim brackets + SearchQueryDTO.parse(str.slice(tokenEnd), implicitOR)] }; } } if (str.startsWith('some-of:') || new RegExp(/^\d*-of:/).test(str)) { const prefix = str.startsWith('some-of:') ? 'some-of:' : new RegExp(/^\d*-of:/).exec(str)[0]; - let tmpList: any = SearchQueryDTO.parse(str.slice(prefix.length + 1, -1)); // trim brackets + let tmpList: any = SearchQueryDTO.parse(str.slice(prefix.length + 1, -1), false); // trim brackets + // console.log(JSON.stringify(tmpList, null, 4)); const unfoldList = (q: SearchListQuery): SearchQueryDTO[] => { if (q.list) { - return [].concat.apply([], q.list.map(e => unfoldList(e))); // flatten array + if (q.type === SearchQueryTypes.UNKNOWN_RELATION) { + return [].concat.apply([], q.list.map(e => unfoldList(e))); // flatten array + } else { + q.list.forEach(e => unfoldList(e)); + } } return [q]; }; diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index c003220e..8c18a759 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -792,7 +792,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { it('should search date', async () => { const sm = new SearchManager(); - let query: any = {value: 0, type: SearchQueryTypes.from_date}; + let query: any = {value: 0, type: SearchQueryTypes.to_date}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ @@ -803,15 +803,15 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = { - value: p.metadata.creationDate, type: SearchQueryTypes.to_date + query = { + value: p.metadata.creationDate, type: SearchQueryTypes.from_date }; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], - media: [p], + media: [p, v], metaFile: [], resultOverflow: false })); @@ -826,7 +826,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], - media: [p2, p_faceLess, p4, v], + media: [p2, p_faceLess, p4], metaFile: [], resultOverflow: false })); @@ -851,7 +851,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { it('should search rating', async () => { const sm = new SearchManager(); - let query: MinRatingSearch | MaxRatingSearch = {value: 0, type: SearchQueryTypes.min_rating}; + let query: MinRatingSearch | MaxRatingSearch = {value: 0, type: SearchQueryTypes.max_rating}; expect(Utils.clone(await sm.search(query))) @@ -888,7 +888,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], - media: [p2], + media: [p2, p_faceLess], metaFile: [], resultOverflow: false })); @@ -898,7 +898,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], - media: [p, p_faceLess], + media: [p], metaFile: [], resultOverflow: false })); @@ -909,7 +909,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { const sm = new SearchManager(); let query: MinResolutionSearch | MaxResolutionSearch = - {value: 0, type: SearchQueryTypes.min_resolution}; + {value: 0, type: SearchQueryTypes.max_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ @@ -930,27 +930,28 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {value: 2, type: SearchQueryTypes.min_resolution}; + query = {value: 3, type: SearchQueryTypes.min_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], - media: [p2, p_faceLess], + media: [p4], + metaFile: [], + resultOverflow: false + })); + + + query = {value: 3, negate: true, type: SearchQueryTypes.min_resolution}; + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p_faceLess, v], metaFile: [], resultOverflow: false })); query = {value: 3, negate: true, type: SearchQueryTypes.max_resolution}; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [p, v, p4], - metaFile: [], - resultOverflow: false - })); - - query = {value: 3, negate: true, type: SearchQueryTypes.min_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, diff --git a/test/common/unit/SearchQueryDTO.ts b/test/common/unit/SearchQueryDTO.ts index 25d2f9f6..d6716680 100644 --- a/test/common/unit/SearchQueryDTO.ts +++ b/test/common/unit/SearchQueryDTO.ts @@ -13,6 +13,7 @@ import { SearchQueryTypes, SomeOfSearchQuery, TextSearch, + TextSearchQueryMatchTypes, ToDateSearch } from '../../../src/common/entities/SearchQueryDTO'; @@ -33,6 +34,11 @@ describe('SearchQueryDTO', () => { check({type: SearchQueryTypes.caption, text: 'caption'}); check({type: SearchQueryTypes.file_name, text: 'filename'}); check({type: SearchQueryTypes.position, text: 'New York'}); + check({ + type: SearchQueryTypes.position, + matchType: TextSearchQueryMatchTypes.exact_match, + text: 'New York' + }); }); it('Date search', () => { @@ -62,6 +68,18 @@ describe('SearchQueryDTO', () => { {type: SearchQueryTypes.position, text: 'New York'} ] }); + + check({ + type: SearchQueryTypes.AND, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + { + type: SearchQueryTypes.position, + matchType: TextSearchQueryMatchTypes.exact_match, + text: 'New York' + } + ] + }); check({ type: SearchQueryTypes.AND, list: [ @@ -75,6 +93,21 @@ describe('SearchQueryDTO', () => { } ] }); + check({ + type: SearchQueryTypes.AND, + list: [ + { + type: SearchQueryTypes.SOME_OF, + min: 2, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.position, text: 'New York'}, + {type: SearchQueryTypes.caption, text: 'caption test'} + ] + }, + {type: SearchQueryTypes.position, text: 'New York'} + ] + }); }); it('Or search', () => { check({ @@ -106,6 +139,21 @@ describe('SearchQueryDTO', () => { {type: SearchQueryTypes.position, text: 'New York'} ] }); + check({ + type: SearchQueryTypes.SOME_OF, + list: [ + { + type: SearchQueryTypes.keyword, + matchType: TextSearchQueryMatchTypes.exact_match, + text: 'big boom' + }, + { + type: SearchQueryTypes.position, + matchType: TextSearchQueryMatchTypes.exact_match, + text: 'New York' + }, + ] + }); check({ type: SearchQueryTypes.SOME_OF, min: 2,